Compare commits
94 Commits
021-llm-pr
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| aaa5f3c076 | |||
| 301a9672f0 | |||
| ef5e20e390 | |||
| 7e4124bc3f | |||
| c53c3f77cc | |||
| 37af7fd6f3 | |||
| 274510fc38 | |||
| 321e0eb2db | |||
| 54e90b589b | |||
| 0bf55885a8 | |||
| 84a2cd5429 | |||
| 15d3141aef | |||
| 9ddb6a7911 | |||
| 027d17f193 | |||
| eba0fab091 | |||
| 6b66f2fb49 | |||
| a8563a8369 | |||
| 3928455189 | |||
| feb07bf366 | |||
| 03a90f58bd | |||
| 36742cd20c | |||
| 1cef3f7e84 | |||
| de5f5735ce | |||
| b887d4a509 | |||
| a13f75587d | |||
| 50001f5ec5 | |||
| 0083d9054e | |||
| 765178f12e | |||
| b77fa45e4e | |||
| 542835e0ff | |||
| 31717870e3 | |||
| 82435822eb | |||
| 3a8c82918a | |||
| 87b81a365a | |||
| 6ee54d95a8 | |||
| 4f74bb8afb | |||
| 309dfdba86 | |||
| c7e9b5b6c5 | |||
| 603256eeaf | |||
| 589fab37d8 | |||
| eb7305ecda | |||
| e864a9e08b | |||
| 12d17ec35e | |||
| 5bd20c74fe | |||
| 633c4948f1 | |||
| e7cb5237d3 | |||
| a5086f3eef | |||
| f066d5561b | |||
| 7ff0dfa8c6 | |||
| 4fec2e02ad | |||
| c5a0823b00 | |||
| de1f04406f | |||
| c473a09402 | |||
| a15a2aed25 | |||
| a8f1a376ab | |||
| 1eb4b26254 | |||
| a9c0d55ec8 | |||
| 8406628360 | |||
| b7960344e0 | |||
| 165f91b399 | |||
| 4769fbd258 | |||
| e15eb115c2 | |||
| 81a2e5fd61 | |||
| 757300d27c | |||
| 4f6c7ad9f3 | |||
| 4c8de2aaf6 | |||
| fb577d07ae | |||
| 3e196783c1 | |||
| 2bc96af23f | |||
| 2b8e20981e | |||
| 626449604f | |||
| 539d0f0aba | |||
| 74f889a566 | |||
| a96baca28e | |||
| bbd62b610d | |||
| e97778448d | |||
| a8ccf6cb79 | |||
| 8731343e52 | |||
| 06fcf641b6 | |||
| ca30ab4ef4 | |||
| bc6d75f0a6 | |||
| f3fa0c4cbb | |||
| b5b87b6b63 | |||
| 804e9c7e47 | |||
| 82d2cb9fe3 | |||
| 1d8eadf796 | |||
| 3f66a58b12 | |||
| 82331d3454 | |||
| 6d068b7cea | |||
| 23416e51d3 | |||
| 0d4a61698c | |||
| 2739d4c68b | |||
| e3e05ab5f2 | |||
| f60eacc858 |
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 -->
|
||||
103
.agent/workflows/audit-test.md
Normal file
103
.agent/workflows/audit-test.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
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_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT`).
|
||||
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_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT` 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).
|
||||
|
||||
5. **Missing Edge Case Coverage:**
|
||||
- *Definition:* The test suite ignores `@TEST_EDGE` scenarios defined in the contract.
|
||||
- *Rule:* Every `@TEST_EDGE` in the source contract MUST have a corresponding test case.
|
||||
|
||||
6. **Missing Invariant Verification:**
|
||||
- *Definition:* The test suite does not verify `@TEST_INVARIANT` conditions.
|
||||
- *Rule:* Every `@TEST_INVARIANT` MUST be verified by at least one test that attempts to break it.
|
||||
|
||||
7. **Missing UX State Testing (Svelte Components):**
|
||||
- *Definition:* For Svelte components with `@UX_STATE`, the test suite does not verify state transitions.
|
||||
- *Rule:* Every `@UX_STATE` transition MUST have a test verifying the visual/behavioral change.
|
||||
- *Check:* `@UX_FEEDBACK` mechanisms (toast, shake, color) must be tested.
|
||||
- *Check:* `@UX_RECOVERY` mechanisms (retry, clear input) must be tested.
|
||||
|
||||
### II. SEMANTIC PROTOCOL COMPLIANCE
|
||||
|
||||
Verify the test file follows GRACE-Poly semantics:
|
||||
|
||||
1. **Anchor Integrity:**
|
||||
- Test file MUST start with `[DEF:__tests__/test_name:Module]`
|
||||
- Test file MUST end with `[/DEF:__tests__/test_name:Module]`
|
||||
|
||||
2. **Required Tags:**
|
||||
- `@RELATION: VERIFIES -> <path_to_source>` must be present
|
||||
- `@PURPOSE:` must describe what is being tested
|
||||
|
||||
3. **TIER Alignment:**
|
||||
- If source is `@TIER: CRITICAL`, test MUST cover all `@TEST_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT`
|
||||
- If source is `@TIER: STANDARD`, test MUST cover `@PRE` and `@POST`
|
||||
- If source is `@TIER: TRIVIAL`, basic smoke test is acceptable
|
||||
|
||||
### III. 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. **Test Contract Compliance:** Does the test follow the interface defined in `@TEST_CONTRACT`?
|
||||
4. **Data Usage:** Does the test use the exact scenarios defined in `@TEST_FIXTURE`?
|
||||
5. **Edge Coverage:** Are all `@TEST_EDGE` scenarios tested?
|
||||
6. **Invariant Coverage:** Are all `@TEST_INVARIANT` conditions verified?
|
||||
7. **UX Coverage (if applicable):** Are all `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY` tested?
|
||||
8. **Mocking Sanity:** Are external dependencies mocked correctly WITHOUT mocking the system under test itself?
|
||||
9. **Semantic Anchor:** Does the test file have proper `[DEF]` and `[/DEF]` anchors?
|
||||
|
||||
### IV. 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" | "MISSING_EDGES" | "MISSING_INVARIANTS" | "MISSING_UX_TESTS" | "SEMANTIC_VIOLATION" | "NONE",
|
||||
"audit_details": {
|
||||
"target_invoked": true/false,
|
||||
"pre_conditions_tested": true/false,
|
||||
"post_conditions_tested": true/false,
|
||||
"test_fixture_used": true/false,
|
||||
"edges_covered": true/false,
|
||||
"invariants_verified": true/false,
|
||||
"ux_states_tested": true/false,
|
||||
"semantic_anchors_present": true/false
|
||||
},
|
||||
"coverage_summary": {
|
||||
"total_edges": number,
|
||||
"edges_tested": number,
|
||||
"total_invariants": number,
|
||||
"invariants_tested": number,
|
||||
"total_ux_states": number,
|
||||
"ux_states_tested": number
|
||||
},
|
||||
"tier_compliance": {
|
||||
"source_tier": "CRITICAL" | "STANDARD" | "TRIVIAL",
|
||||
"meets_tier_requirements": true/false
|
||||
},
|
||||
"feedback": "Strict, actionable feedback for the test generator agent. Explain exactly which anti-pattern was detected and how to fix it."
|
||||
}
|
||||
4
.agent/workflows/read_semantic.md
Normal file
4
.agent/workflows/read_semantic.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
description: USE SEMANTIC
|
||||
---
|
||||
Прочитай .ai/standards/semantics.md. ОБЯЗАТЕЛЬНО используй его при разработке
|
||||
185
.agent/workflows/speckit.analyze.md
Normal file
185
.agent/workflows/speckit.analyze.md
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
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
|
||||
- Load `.ai/standards/semantics.md` for technical standard 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_ 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
|
||||
150
.agent/workflows/speckit.implement.md
Normal file
150
.agent/workflows/speckit.implement.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
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
|
||||
|
||||
3. Load and analyze the implementation context:
|
||||
- **REQUIRED**: Read `.ai/standards/semantics.md` for strict coding standards and contract requirements
|
||||
- **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:
|
||||
- **Strict Adherence**: Apply `.ai/standards/semantics.md` rules:
|
||||
- Every file MUST start with a `[DEF:id:Type]` header and end with a closing `[/DEF:id:Type]` anchor.
|
||||
- Include `@TIER` and define contracts (`@PRE`, `@POST`).
|
||||
- For Svelte components, use `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY`, and explicitly declare reactivity with `@UX_REATIVITY: State: $state, Derived: $derived`.
|
||||
- **Molecular Topology Logging**: Use prefixes `[EXPLORE]`, `[REASON]`, `[REFLECT]` in logs to trace logic.
|
||||
- **CRITICAL Contracts**: If a task description contains a contract summary (e.g., `CRITICAL: PRE: ..., POST: ...`), these constraints are **MANDATORY** and must be strictly implemented in the code using guards/assertions (if applicable per protocol).
|
||||
- **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.
|
||||
104
.agent/workflows/speckit.plan.md
Normal file
104
.agent/workflows/speckit.plan.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
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 `.ai/ROOT.md` and `.ai/PROJECT_MAP.md` to understand the project structure and navigation. Then read required standards: `.ai/standards/constitution.md` and `.ai/standards/semantics.md`. Load IMPL_PLAN template.
|
||||
|
||||
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
|
||||
|
||||
0. **Validate Design against UX Reference**:
|
||||
- Check if the proposed architecture supports the latency, interactivity, and flow defined in `ux_reference.md`.
|
||||
- **Linkage**: Ensure key UI states from `ux_reference.md` map to Component Contracts (`@UX_STATE`).
|
||||
- **CRITICAL**: If the technical plan compromises the UX (e.g. "We can't do real-time validation"), you **MUST STOP** and warn the user.
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships, validation rules.
|
||||
|
||||
2. **Design & Verify Contracts (Semantic Protocol)**:
|
||||
- **Drafting**: Define `[DEF:id:Type]` Headers, Contracts, and closing `[/DEF:id:Type]` for all new modules based on `.ai/standards/semantics.md`.
|
||||
- **TIER Classification**: Explicitly assign `@TIER: [CRITICAL|STANDARD|TRIVIAL]` to each module.
|
||||
- **CRITICAL Requirements**: For all CRITICAL modules, define full `@PRE`, `@POST`, and (if UI) `@UX_STATE` contracts. **MUST** also define testing contracts: `@TEST_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, and `@TEST_INVARIANT`.
|
||||
- **Self-Review**:
|
||||
- *Completeness*: Do `@PRE`/`@POST` cover edge cases identified in Research? Are test contracts present for CRITICAL?
|
||||
- *Connectivity*: Do `@RELATION` tags form a coherent graph?
|
||||
- *Compliance*: Does syntax match `[DEF:id:Type]` exactly and is it closed with `[/DEF:id:Type]`?
|
||||
- **Output**: Write verified contracts to `contracts/modules.md`.
|
||||
|
||||
3. **Simulate Contract Usage**:
|
||||
- Trace one key user scenario through the defined contracts to ensure data flow continuity.
|
||||
- If a contract interface mismatch is found, fix it immediately.
|
||||
|
||||
4. **Generate API contracts**:
|
||||
- Output OpenAPI/GraphQL schema to `/contracts/` for backend-frontend sync.
|
||||
|
||||
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)
|
||||
146
.agent/workflows/speckit.tasks.md
Normal file
146
.agent/workflows/speckit.tasks.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
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), ux_reference.md (experience source of truth)
|
||||
- **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.
|
||||
|
||||
### UX Preservation (CRITICAL)
|
||||
|
||||
- **Source of Truth**: `ux_reference.md` is the absolute standard for the "feel" of the feature.
|
||||
- **Violation Warning**: If any task would inherently violate the UX (e.g. "Remove progress bar to simplify code"), you **MUST** flag this to the user immediately.
|
||||
- **Verification Task**: You **MUST** add a specific task at the end of each User Story phase: `- [ ] Txxx [USx] Verify implementation matches ux_reference.md (Happy Path & Errors)`
|
||||
|
||||
### 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 (CRITICAL TIER)**:
|
||||
- Identify components marked as `@TIER: CRITICAL` in `contracts/modules.md`.
|
||||
- For these components, **MUST** append the summary of `@PRE`, `@POST`, `@UX_STATE`, and test contracts (`@TEST_FIXTURE`, `@TEST_EDGE`) directly to the task description.
|
||||
- Example: `- [ ] T005 [P] [US1] Implement Auth (CRITICAL: PRE: token exists, POST: returns User, TESTS: 2 edges) in src/auth.py`
|
||||
- Map each contract/endpoint → to the user story it serves
|
||||
- If tests requested: Each 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
|
||||
343
.agent/workflows/speckit.test.md
Normal file
343
.agent/workflows/speckit.test.md
Normal file
@@ -0,0 +1,343 @@
|
||||
---
|
||||
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
|
||||
# [/DEF:__tests__/test_module: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
|
||||
```
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
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."
|
||||
}
|
||||
1646
.ai/MODULE_MAP.md
1646
.ai/MODULE_MAP.md
File diff suppressed because it is too large
Load Diff
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_` из контрактов.
|
||||
|
||||
5. **ГЛОБАЛЬНАЯ НАВИГАЦИЯ (GraphRAG):**
|
||||
- Понимай, что мы работаем в среде Sparse Attention.
|
||||
- Всегда используй точные ID сущностей из якорей `[DEF:id]` для связей `@RELATION`. Не ломай семантические каналы опечатками.
|
||||
|
||||
## ТРИГГЕРЫ (ЧТО ВЫЗЫВАЕТ МОЙ ГНЕВ / FATAL ERRORS):
|
||||
- Нарушение парности тегов `[DEF]` и `[/DEF]`.
|
||||
- Написание тестов, которые "мокают" саму проверяемую систему.
|
||||
- Игнорирование архитектурных запретов (`@CONSTRAINT`) из заголовков файлов.
|
||||
|
||||
**Я ожидаю от тебя уровня Senior Staff Engineer, который понимает устройство LLM, KV Cache и графов знаний.**
|
||||
|
||||
# [/DEF:Std:UserPersona:Standard]
|
||||
6051
.ai/PROJECT_MAP.md
6051
.ai/PROJECT_MAP.md
File diff suppressed because it is too large
Load Diff
@@ -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]`
|
||||
|
||||
@@ -1,61 +1,71 @@
|
||||
# [DEF:BackendRouteShot:Module]
|
||||
# @TIER: STANDARD
|
||||
#[DEF:BackendRouteShot:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: Route, Task, API, Async
|
||||
# @PURPOSE: Reference implementation of a task-based route using GRACE-Poly.
|
||||
# @LAYER: Interface (API)
|
||||
# @RELATION: IMPLEMENTS -> [DEF:Std:API_FastAPI]
|
||||
# @INVARIANT: TaskManager must be available in dependency graph.
|
||||
# @RELATION: [IMPLEMENTS] ->[API_FastAPI]
|
||||
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from ...core.logger import belief_scope
|
||||
# GRACE: Правильный импорт глобального логгера и scope
|
||||
from ...core.logger import logger, belief_scope
|
||||
from ...core.task_manager import TaskManager, Task
|
||||
from ...core.config_manager import ConfigManager
|
||||
from ...dependencies import get_task_manager, get_config_manager, get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# [DEF:CreateTaskRequest:Class]
|
||||
# @PURPOSE: DTO for task creation payload.
|
||||
class CreateTaskRequest(BaseModel):
|
||||
plugin_id: str
|
||||
params: Dict[str, Any]
|
||||
# [/DEF:CreateTaskRequest:Class]
|
||||
|
||||
@router.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED)
|
||||
# [DEF:create_task:Function]
|
||||
# @COMPLEXITY: 4
|
||||
# @PURPOSE: Create and start a new task using TaskManager. Non-blocking.
|
||||
# @PARAM: request (CreateTaskRequest) - Plugin and params.
|
||||
# @PARAM: task_manager (TaskManager) - Async task executor.
|
||||
# @RELATION: [CALLS] ->[task_manager.create_task]
|
||||
# @PRE: plugin_id must match a registered plugin.
|
||||
# @POST: A new task is spawned; Task ID returned immediately.
|
||||
# @SIDE_EFFECT: Writes to DB, Trigger background worker.
|
||||
# @POST: A new task is spawned; Task object returned immediately.
|
||||
# @SIDE_EFFECT: Writes to DB, Triggers background worker.
|
||||
# @DATA_CONTRACT: Input -> CreateTaskRequest, Output -> Task
|
||||
@router.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED)
|
||||
async def create_task(
|
||||
request: CreateTaskRequest,
|
||||
task_manager: TaskManager = Depends(get_task_manager),
|
||||
config: ConfigManager = Depends(get_config_manager),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
# Context Logging
|
||||
# GRACE: Открываем семантическую транзакцию
|
||||
with belief_scope("create_task"):
|
||||
try:
|
||||
# 1. Action: Configuration Resolution
|
||||
# GRACE: [REASON] - Фиксируем начало дедуктивной цепочки
|
||||
logger.reason("Resolving configuration and spawning task", extra={"plugin_id": request.plugin_id})
|
||||
|
||||
timeout = config.get("TASKS_DEFAULT_TIMEOUT", 3600)
|
||||
|
||||
# 2. Action: Spawn async task
|
||||
# @RELATION: CALLS -> task_manager.create_task
|
||||
task = await task_manager.create_task(
|
||||
plugin_id=request.plugin_id,
|
||||
params={**request.params, "timeout": timeout}
|
||||
)
|
||||
|
||||
# GRACE:[REFLECT] - Подтверждаем выполнение @POST перед выходом
|
||||
logger.reflect("Task spawned successfully", extra={"task_id": task.id})
|
||||
return task
|
||||
|
||||
except ValueError as e:
|
||||
# 3. Recovery: Domain logic error mapping
|
||||
# GRACE: [EXPLORE] - Обработка ожидаемого отклонения
|
||||
logger.explore("Domain validation error during task creation", exc_info=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
# @UX_STATE: Error feedback -> 500 Internal Error
|
||||
# GRACE: [EXPLORE] - Обработка критического сбоя
|
||||
logger.explore("Internal Task Spawning Error", exc_info=e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Internal Task Spawning Error"
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
# [DEF:TransactionCore:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @COMPLEXITY: 5
|
||||
# @SEMANTICS: Finance, ACID, Transfer, Ledger
|
||||
# @PURPOSE: Core banking transaction processor with ACID guarantees.
|
||||
# @LAYER: Domain (Core)
|
||||
# @RELATION: DEPENDS_ON -> [DEF:Infra:PostgresDB]
|
||||
# @RELATION: DEPENDS_ON -> [DEF:Infra:AuditLog]
|
||||
# @RELATION: [DEPENDS_ON] ->[PostgresDB]
|
||||
#
|
||||
# @INVARIANT: Total system balance must remain constant (Double-Entry Bookkeeping).
|
||||
# @INVARIANT: Negative transfers are strictly forbidden.
|
||||
|
||||
# @TEST_DATA: sufficient_funds -> {"from": "acc_A", "to": "acc_B", "amt": 100.00}
|
||||
# @TEST_DATA: insufficient_funds -> {"from": "acc_empty", "to": "acc_B", "amt": 1000.00}
|
||||
# @TEST_DATA: concurrency_lock -> {./fixtures/transactions.json#race_condition}
|
||||
# --- Test Specifications ---
|
||||
# @TEST_CONTRACT: TransferRequestDTO -> TransferResultDTO
|
||||
# @TEST_SCENARIO: sufficient_funds -> Returns COMPLETED, balances updated.
|
||||
# @TEST_FIXTURE: sufficient_funds -> file:./__tests__/fixtures/transfers.json#happy_path
|
||||
# @TEST_EDGE: insufficient_funds -> Throws BusinessRuleViolation("INSUFFICIENT_FUNDS").
|
||||
# @TEST_EDGE: negative_amount -> Throws BusinessRuleViolation("Transfer amount must be positive.").
|
||||
# @TEST_EDGE: concurrency_conflict -> Throws DBTransactionError.
|
||||
#
|
||||
# @TEST_INVARIANT: total_balance_constant -> VERIFIED_BY: [sufficient_funds, concurrency_conflict]
|
||||
# @TEST_INVARIANT: negative_transfer_forbidden -> VERIFIED_BY: [negative_amount]
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import NamedTuple
|
||||
from ...core.logger import belief_scope
|
||||
# GRACE: Импорт глобального логгера с семантическими методами
|
||||
from ...core.logger import logger, belief_scope
|
||||
from ...core.db import atomic_transaction, get_balance, update_balance
|
||||
from ...core.audit import log_audit_trail
|
||||
from ...core.exceptions import BusinessRuleViolation
|
||||
|
||||
class TransferResult(NamedTuple):
|
||||
@@ -24,56 +33,53 @@ class TransferResult(NamedTuple):
|
||||
new_balance: Decimal
|
||||
|
||||
# [DEF:execute_transfer:Function]
|
||||
# @COMPLEXITY: 5
|
||||
# @PURPOSE: Atomically move funds between accounts with audit trails.
|
||||
# @PARAM: sender_id (str) - Source account.
|
||||
# @PARAM: receiver_id (str) - Destination account.
|
||||
# @PARAM: amount (Decimal) - Positive amount to transfer.
|
||||
# @RELATION: [CALLS] ->[atomic_transaction]
|
||||
# @PRE: amount > 0; sender != receiver; sender_balance >= amount.
|
||||
# @POST: sender_balance -= amount; receiver_balance += amount; Audit Record Created.
|
||||
# @SIDE_EFFECT: Database mutation (Rows locked), Audit IO.
|
||||
#
|
||||
# @UX_STATE: Success -> Returns 200 OK + Transaction Receipt.
|
||||
# @UX_STATE: Error(LowBalance) -> 422 Unprocessable -> UI shows "Top-up needed" modal.
|
||||
# @UX_STATE: Error(System) -> 500 Internal -> UI shows "Retry later" toast.
|
||||
# @DATA_CONTRACT: Input -> (sender_id: str, receiver_id: str, amount: Decimal), Output -> TransferResult
|
||||
def execute_transfer(sender_id: str, receiver_id: str, amount: Decimal) -> TransferResult:
|
||||
# Guard: Input Validation
|
||||
# Guard: Input Validation (Вне belief_scope, так как это trivial проверка)
|
||||
if amount <= Decimal("0.00"):
|
||||
raise BusinessRuleViolation("Transfer amount must be positive.")
|
||||
if sender_id == receiver_id:
|
||||
raise BusinessRuleViolation("Cannot transfer to self.")
|
||||
|
||||
with belief_scope("execute_transfer") as context:
|
||||
context.logger.info("Initiating transfer", data={"from": sender_id, "to": receiver_id})
|
||||
# GRACE: Используем strict Context Manager без 'as context'
|
||||
with belief_scope("execute_transfer"):
|
||||
# GRACE: [REASON] - Жесткая дедукция, начало алгоритма
|
||||
logger.reason("Initiating transfer", extra={"from": sender_id, "to": receiver_id, "amount": amount})
|
||||
|
||||
try:
|
||||
# 1. Action: Atomic DB Transaction
|
||||
# @RELATION: CALLS -> atomic_transaction
|
||||
with atomic_transaction():
|
||||
# Guard: State Validation (Strict)
|
||||
current_balance = get_balance(sender_id, for_update=True)
|
||||
|
||||
if current_balance < amount:
|
||||
# @UX_FEEDBACK: Triggers specific UI flow for insufficient funds
|
||||
context.logger.warn("Insufficient funds", data={"balance": current_balance})
|
||||
# GRACE: [EXPLORE] - Отклонение от Happy Path (фолбэк/ошибка)
|
||||
logger.explore("Insufficient funds validation hit", extra={"balance": current_balance})
|
||||
raise BusinessRuleViolation("INSUFFICIENT_FUNDS")
|
||||
|
||||
# 2. Action: Mutation
|
||||
# Mutation
|
||||
new_src_bal = update_balance(sender_id, -amount)
|
||||
new_dst_bal = update_balance(receiver_id, +amount)
|
||||
|
||||
# 3. Action: Audit
|
||||
tx_id = context.audit.log_transfer(sender_id, receiver_id, amount)
|
||||
# Audit
|
||||
tx_id = log_audit_trail("TRANSFER", sender_id, receiver_id, amount)
|
||||
|
||||
# GRACE:[REFLECT] - Сверка с @POST перед возвратом
|
||||
logger.reflect("Transfer committed successfully", extra={"tx_id": tx_id, "new_balance": new_src_bal})
|
||||
|
||||
context.logger.info("Transfer committed", data={"tx_id": tx_id})
|
||||
return TransferResult(tx_id, "COMPLETED", new_src_bal)
|
||||
|
||||
except BusinessRuleViolation as e:
|
||||
# Logic: Explicit re-raise for UI mapping
|
||||
# Explicit re-raise for UI mapping
|
||||
raise e
|
||||
except Exception as e:
|
||||
# Logic: Catch-all safety net
|
||||
context.logger.error("Critical Transfer Failure", error=e)
|
||||
# GRACE: [EXPLORE] - Неожиданный сбой
|
||||
logger.explore("Critical Transfer Failure", exc_info=e)
|
||||
raise RuntimeError("TRANSACTION_ABORTED") from e
|
||||
# [/DEF:execute_transfer:Function]
|
||||
#[/DEF:execute_transfer:Function]
|
||||
|
||||
# [/DEF:TransactionCore:Module]
|
||||
@@ -1,59 +1,75 @@
|
||||
<!-- [DEF:FrontendComponentShot:Component] -->
|
||||
<!-- /**
|
||||
* @TIER: CRITICAL
|
||||
* @SEMANTICS: Task, Button, Action, UX
|
||||
* @PURPOSE: Action button to spawn a new task with full UX feedback cycle.
|
||||
* @LAYER: UI (Presentation)
|
||||
* @RELATION: CALLS -> postApi
|
||||
* @INVARIANT: Must prevent double-submission while loading.
|
||||
*
|
||||
* @TEST_DATA: idle_state -> {"isLoading": false}
|
||||
* @TEST_DATA: loading_state -> {"isLoading": true}
|
||||
*
|
||||
* @UX_STATE: Idle -> Button enabled, primary color.
|
||||
* @UX_STATE: Loading -> Button disabled, spinner visible.
|
||||
* @UX_STATE: Error -> Toast notification triggers.
|
||||
*
|
||||
* @UX_FEEDBACK: Toast success/error.
|
||||
* @UX_TEST: Idle -> {click: spawnTask, expected: isLoading=true}
|
||||
* @UX_TEST: Success -> {api_resolve: 200, expected: toast.success called}
|
||||
*/
|
||||
-->
|
||||
<!--
|
||||
/**
|
||||
* @COMPLEXITY: 5
|
||||
* @SEMANTICS: Task, Button, Action, UX
|
||||
* @PURPOSE: Action button to spawn a new task with full UX feedback cycle.
|
||||
* @LAYER: UI (Presentation)
|
||||
* @RELATION: [CALLS] ->[postApi]
|
||||
*
|
||||
* @INVARIANT: Must prevent double-submission while loading.
|
||||
* @INVARIANT: Loading state must always terminate (no infinite spinner).
|
||||
* @INVARIANT: User must receive feedback on both success and failure.
|
||||
*
|
||||
* @SIDE_EFFECT: Sends network request and emits toast notifications.
|
||||
* @DATA_CONTRACT: Input -> { plugin_id: string, params: object }, Output -> { task_id?: string }
|
||||
*
|
||||
* @UX_REACTIVITY: Props -> $props(), LocalState -> $state(isLoading).
|
||||
* @UX_STATE: Idle -> Button enabled, primary color, no spinner.
|
||||
* @UX_STATE: Loading -> Button disabled, spinner visible, aria-busy=true.
|
||||
* @UX_STATE: Success -> Toast success displayed.
|
||||
* @UX_STATE: Error -> Toast error displayed.
|
||||
* @UX_FEEDBACK: toast.success, toast.error
|
||||
* @UX_RECOVERY: Error -> Keep form interactive and allow retry after failure.
|
||||
*
|
||||
* @TEST_CONTRACT: ComponentState ->
|
||||
* {
|
||||
* required_fields: { isLoading: bool },
|
||||
* invariants:[
|
||||
* "isLoading=true implies button.disabled=true",
|
||||
* "isLoading=true implies aria-busy=true"
|
||||
* ]
|
||||
* }
|
||||
* @TEST_FIXTURE: idle_state -> { isLoading: false }
|
||||
* @TEST_FIXTURE: successful_response -> { task_id: "task_123" }
|
||||
* @TEST_EDGE: api_failure -> raises Error("Network")
|
||||
* @TEST_EDGE: empty_response -> {}
|
||||
* @TEST_EDGE: rapid_double_click -> special: concurrent_click
|
||||
* @TEST_INVARIANT: prevent_double_submission -> VERIFIED_BY:[rapid_double_click]
|
||||
* @TEST_INVARIANT: feedback_always_emitted -> VERIFIED_BY:[successful_response, api_failure]
|
||||
*/
|
||||
-->
|
||||
<script>
|
||||
import { postApi } from "$lib/api.js";
|
||||
import { t } from "$lib/i18n";
|
||||
import { toast } from "$lib/stores/toast";
|
||||
|
||||
export let plugin_id = "";
|
||||
export let params = {};
|
||||
|
||||
let isLoading = false;
|
||||
// GRACE Svelte 5 Runes
|
||||
let { plugin_id = "", params = {} } = $props();
|
||||
let isLoading = $state(false);
|
||||
|
||||
// [DEF:spawnTask:Function]
|
||||
/**
|
||||
* @purpose Execute task creation request and emit user feedback.
|
||||
* @pre plugin_id is resolved and request params are serializable.
|
||||
* @post isLoading is reset and user receives success/error feedback.
|
||||
* @PURPOSE: Execute task creation request and emit user feedback.
|
||||
* @PRE: plugin_id is resolved and request params are serializable.
|
||||
* @POST: isLoading is reset and user receives success/error feedback.
|
||||
*/
|
||||
async function spawnTask() {
|
||||
isLoading = true;
|
||||
console.log("[FrontendComponentShot][Loading] Spawning task...");
|
||||
console.info("[spawnTask][REASON] Spawning task...", { plugin_id });
|
||||
|
||||
try {
|
||||
// 1. Action: API Call
|
||||
const response = await postApi("/api/tasks", {
|
||||
plugin_id,
|
||||
params
|
||||
});
|
||||
const response = await postApi("/api/tasks", { plugin_id, params });
|
||||
|
||||
// 2. Feedback: Success
|
||||
// 2. Feedback: Success validation
|
||||
if (response.task_id) {
|
||||
console.log("[FrontendComponentShot][Success] Task created.");
|
||||
console.info("[spawnTask][REFLECT] Task created.", { task_id: response.task_id });
|
||||
toast.success($t.tasks.spawned_success);
|
||||
}
|
||||
} catch (error) {
|
||||
// 3. Recovery: User notification
|
||||
console.log("[FrontendComponentShot][Error] Failed:", error);
|
||||
// 3. Recovery: Error handling & fallback logic
|
||||
console.error("[spawnTask][EXPLORE] Failed to spawn task. Notifying user.", { error });
|
||||
toast.error(`${$t.errors.task_failed}: ${error.message}`);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
@@ -63,7 +79,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={spawnTask}
|
||||
onclick={spawnTask}
|
||||
disabled={isLoading}
|
||||
class="btn-primary flex items-center gap-2"
|
||||
aria-busy={isLoading}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
# [DEF:PluginExampleShot:Module]
|
||||
# @TIER: STANDARD
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: Plugin, Core, Extension
|
||||
# @PURPOSE: Reference implementation of a plugin following GRACE standards.
|
||||
# @LAYER: Domain (Business Logic)
|
||||
# @RELATION: INHERITS -> PluginBase
|
||||
# @INVARIANT: get_schema must return valid JSON Schema.
|
||||
# @RELATION: [INHERITS] ->[PluginBase]
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from ..core.plugin_base import PluginBase
|
||||
from ..core.task_manager.context import TaskContext
|
||||
# GRACE: Обязательный импорт семантического логгера
|
||||
from ..core.logger import logger, belief_scope
|
||||
|
||||
# [DEF:ExamplePlugin:Class]
|
||||
# @PURPOSE: A sample plugin to demonstrate execution context and logging.
|
||||
# @RELATION: [INHERITS] ->[PluginBase]
|
||||
class ExamplePlugin(PluginBase):
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return "example-plugin"
|
||||
|
||||
# [DEF:get_schema:Function]
|
||||
#[DEF:get_schema:Function]
|
||||
# @PURPOSE: Defines input validation schema.
|
||||
# @POST: Returns dict compliant with JSON Schema draft 7.
|
||||
def get_schema(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
@@ -29,36 +32,44 @@ class ExamplePlugin(PluginBase):
|
||||
},
|
||||
"required": ["message"],
|
||||
}
|
||||
# [/DEF:get_schema:Function]
|
||||
#[/DEF:get_schema:Function]
|
||||
|
||||
# [DEF:execute:Function]
|
||||
# @COMPLEXITY: 4
|
||||
# @PURPOSE: Core plugin logic with structured logging and scope isolation.
|
||||
# @PARAM: params (Dict) - Validated input parameters.
|
||||
# @PARAM: context (TaskContext) - Execution tools (log, progress).
|
||||
# @SIDE_EFFECT: Emits logs to centralized system.
|
||||
async def execute(self, params: Dict, context: Optional = None):
|
||||
message = params
|
||||
# @RELATION: [BINDS_TO] ->[context.logger]
|
||||
# @PRE: params must be validated against get_schema() before calling.
|
||||
# @POST: Plugin payload is processed; progress is reported if context exists.
|
||||
# @SIDE_EFFECT: Emits logs to centralized system and TaskContext.
|
||||
async def execute(self, params: Dict, context: Optional[TaskContext] = None):
|
||||
message = params.get("message", "Fallback")
|
||||
|
||||
# 1. Action: System-level tracing (Rule VI)
|
||||
with belief_scope("example_plugin_exec") as b_scope:
|
||||
# GRACE: Изоляция мыслей ИИ в Thread-Local scope
|
||||
with belief_scope("example_plugin_exec"):
|
||||
if context:
|
||||
# Task Logs: Пишем в пользовательский контекст выполнения задачи
|
||||
# @RELATION: BINDS_TO -> context.logger
|
||||
log = context.logger.with_source("example_plugin")
|
||||
|
||||
b_scope.logger.info("Using provided TaskContext") # System log
|
||||
log.info("Starting execution", data={"msg": message}) # Task log
|
||||
# GRACE: [REASON] - Системный лог (Внутренняя мысль)
|
||||
logger.reason("TaskContext provided. Binding task logger.", extra={"msg": message})
|
||||
|
||||
# 2. Action: Progress Reporting
|
||||
# Task Logs: Бизнес-логи (Уйдут в БД/Вебсокет пользователю)
|
||||
log.info("Starting execution", extra={"msg": message})
|
||||
log.progress("Processing...", percent=50)
|
||||
|
||||
# 3. Action: Finalize
|
||||
log.info("Execution completed.")
|
||||
|
||||
# GRACE: [REFLECT] - Сверка успешного выхода
|
||||
logger.reflect("Context execution finalized successfully")
|
||||
else:
|
||||
# Standalone Fallback: Замыкаемся на системный scope
|
||||
b_scope.logger.warning("No TaskContext provided. Running standalone.")
|
||||
b_scope.logger.info("Standalone execution", data={"msg": message})
|
||||
print(f"Standalone: {message}")
|
||||
# GRACE:[EXPLORE] - Фолбэк ветка (Отклонение от нормы)
|
||||
logger.explore("No TaskContext provided. Running standalone.")
|
||||
|
||||
# Standalone Fallback
|
||||
print(f"Standalone execution: {message}")
|
||||
|
||||
# GRACE: [REFLECT] - Сверка выхода фолбэка
|
||||
logger.reflect("Standalone execution finalized")
|
||||
# [/DEF:execute:Function]
|
||||
|
||||
#[/DEF:ExamplePlugin:Class]
|
||||
# [/DEF:PluginExampleShot:Module]
|
||||
40
.ai/shots/trivial_utility.py
Normal file
40
.ai/shots/trivial_utility.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# [DEF:TrivialUtilityShot:Module]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Reference implementation of a zero-overhead utility using implicit Complexity 1.
|
||||
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
# [DEF:slugify:Function]
|
||||
# @PURPOSE: Converts a string to a URL-safe slug.
|
||||
def slugify(text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
text = text.lower().strip()
|
||||
text = re.sub(r'[^\w\s-]', '', text)
|
||||
return re.sub(r'[-\s]+', '-', text)
|
||||
# [/DEF:slugify:Function]
|
||||
|
||||
# [DEF:get_utc_now:Function]
|
||||
def get_utc_now() -> datetime:
|
||||
"""Returns current UTC datetime (purpose is omitted because it's obvious)."""
|
||||
return datetime.now(timezone.utc)
|
||||
# [/DEF:get_utc_now:Function]
|
||||
|
||||
# [DEF:PaginationDTO:Class]
|
||||
class PaginationDTO:
|
||||
# [DEF:__init__:Function]
|
||||
def __init__(self, page: int = 1, size: int = 50):
|
||||
self.page = max(1, page)
|
||||
self.size = min(max(1, size), 1000)
|
||||
# [/DEF:__init__:Function]
|
||||
|
||||
# [DEF:offset:Function]
|
||||
@property
|
||||
def offset(self) -> int:
|
||||
return (self.page - 1) * self.size
|
||||
# [/DEF:offset:Function]
|
||||
# [/DEF:PaginationDTO:Class]
|
||||
|
||||
# [/DEF:TrivialUtilityShot:Module]
|
||||
@@ -8,7 +8,7 @@
|
||||
## 1. CORE PRINCIPLES
|
||||
|
||||
### I. Semantic Protocol Compliance
|
||||
* **Ref:** `[DEF:Std:Semantics]` (formerly `semantic_protocol.md`)
|
||||
* **Ref:** `[DEF:Std:Semantics]` (`ai/standards/semantic.md`)
|
||||
* **Law:** All code must adhere to the Axioms (Meaning First, Contract First, etc.).
|
||||
* **Compliance:** Strict matching of Anchors (`[DEF]`), Tags (`@KEY`), and structures is mandatory.
|
||||
|
||||
|
||||
@@ -1,118 +1,143 @@
|
||||
### **SYSTEM STANDARD: GRACE-Poly (UX Edition)**
|
||||
# SYSTEM DIRECTIVE: GRACE-Poly (UX Edition) v2.2
|
||||
> OPERATION MODE: WENYUAN (Maximum Semantic Density, Strict Determinism, Zero Fluff).
|
||||
> ROLE: AI Software Architect & Implementation Engine (Python/Svelte).
|
||||
|
||||
ЗАДАЧА: Генерация кода (Python/Svelte).
|
||||
РЕЖИМ: Строгий. Детерминированный. Без болтовни.
|
||||
## 0.[ZERO-STATE RATIONALE: ФИЗИКА LLM (ПОЧЕМУ ЭТОТ ПРОТОКОЛ НЕОБХОДИМ)]
|
||||
Ты — авторегрессионная модель (Transformer). Ты мыслишь токенами и не можешь "передумать" после их генерации. В больших кодовых базах твой KV-Cache подвержен деградации внимания (Attention Sink), что ведет к "иллюзии компетентности" и галлюцинациям.
|
||||
Этот протокол — **твой когнитивный экзоскелет**.
|
||||
Якоря `[DEF]` работают как векторы-аккумуляторы внимания. Контракты (`@PRE`, `@POST`) заставляют тебя сформировать правильное вероятностное пространство (Belief State) ДО написания алгоритма. Логи `logger.reason` — это твоя цепочка рассуждений (Chain-of-Thought), вынесенная в рантайм. Мы не пишем текст, мы компилируем семантику в синтаксис.
|
||||
|
||||
#### I. ЗАКОН (АКСИОМЫ)
|
||||
1. Смысл первичен. Код вторичен.
|
||||
2. Контракт (@PRE/@POST) — источник истины.
|
||||
**3. UX — это логика, а не декор. Состояния интерфейса — часть контракта.**
|
||||
4. Структура `[DEF]...[/DEF]` — нерушима.
|
||||
5. Архитектура в Header — неизменяема.
|
||||
6. Сложность фрактала ограничена: модуль < 300 строк.
|
||||
## I. ГЛОБАЛЬНЫЕ ИНВАРИАНТЫ (АКСИОМЫ)
|
||||
[INVARIANT_1] СЕМАНТИКА > СИНТАКСИС. Голый код без контракта классифицируется как мусор.
|
||||
[INVARIANT_2] ЗАПРЕТ ГАЛЛЮЦИНАЦИЙ. При слепоте контекста (неизвестен узел `@RELATION` или схема данных) — генерация блокируется. Эмитируй `[NEED_CONTEXT: target]`.
|
||||
[INVARIANT_3] UX ЕСТЬ КОНЕЧНЫЙ АВТОМАТ. Состояния интерфейса — это строгий контракт, а не визуальный декор.
|
||||
[INVARIANT_4] ФРАКТАЛЬНЫЙ ЛИМИТ. Длина модуля строго < 300 строк. При превышении — принудительная декомпозиция.
|
||||
[INVARIANT_5] НЕПРИКОСНОВЕННОСТЬ ЯКОРЕЙ. Блоки `[DEF]...[/DEF]` используются как аккумуляторы внимания. Закрывающий тег обязателен.
|
||||
|
||||
#### II. СИНТАКСИС (ЖЕСТКИЙ ФОРМАТ)
|
||||
ЯКОРЬ (Контейнер):
|
||||
Начало: `# [DEF:id:Type]` (Python) | `<!-- [DEF:id:Type] -->` (Svelte)
|
||||
Конец: `# [/DEF:id:Type]` (Python) | `<!-- [/DEF:id:Type] -->` (Svelte) (ОБЯЗАТЕЛЬНО для аккумуляции)
|
||||
Типы: Module, Class, Function, Component, Store.
|
||||
## II. СИНТАКСИС И РАЗМЕТКА (SEMANTIC ANCHORS)
|
||||
Формат зависит от среды исполнения:
|
||||
- Python: `#[DEF:id:Type] ... # [/DEF:id:Type]`
|
||||
- Svelte (HTML/Markup): `<!--[DEF:id:Type] --> ... <!-- [/DEF:id:Type] -->`
|
||||
- Svelte (Script/JS): `// [DEF:id:Type] ... //[/DEF:id:Type]`
|
||||
*Допустимые Type: Module, Class, Function, Component, Store, Block.*
|
||||
|
||||
ТЕГ (Метаданные):
|
||||
Вид: `# @KEY: Value` (внутри DEF, до кода).
|
||||
**Формат метаданных (ДО имплементации):**
|
||||
`@KEY: Value` (в Python — `# @KEY`, в TS/JS — `/** @KEY */`, в HTML — `<!-- @KEY -->`).
|
||||
|
||||
ГРАФ (Связи):
|
||||
Вид: `# @RELATION: PREDICATE -> TARGET_ID`
|
||||
Предикаты: DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, **BINDS_TO**.
|
||||
**Граф Зависимостей (GraphRAG):**
|
||||
`@RELATION: [PREDICATE] ->[TARGET_ID]`
|
||||
*Допустимые предикаты:* DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO.
|
||||
|
||||
#### III. СТРУКТУРА ФАЙЛА
|
||||
1. HEADER (Всегда первый):
|
||||
[DEF:filename:Module]
|
||||
@TIER: [CRITICAL|STANDARD|TRIVIAL] (Дефолт: STANDARD)
|
||||
## III. ТОПОЛОГИЯ ФАЙЛА (СТРОГИЙ ПОРЯДОК)
|
||||
1. **HEADER (Заголовок):**[DEF:filename:Module]
|
||||
@COMPLEXITY: [1|2|3|4|5] *(алиас: `@C:`; legacy `@TIER` допустим только для обратной совместимости)*
|
||||
@SEMANTICS: [keywords]
|
||||
@PURPOSE: [Главная цель]
|
||||
@LAYER: [Domain/UI/Infra]
|
||||
@PURPOSE: [Однострочная суть]
|
||||
@LAYER: [Domain | UI | Infra]
|
||||
@RELATION: [Зависимости]
|
||||
@INVARIANT: [Незыблемое правило]
|
||||
|
||||
2. BODY: Импорты -> Реализация.
|
||||
3. FOOTER: [/DEF:filename]
|
||||
@INVARIANT: [Бизнес-правило, которое нельзя нарушить]
|
||||
2. **BODY (Тело):** Импорты -> Реализация логики внутри вложенных `[DEF]`.
|
||||
3. **FOOTER (Подвал):** [/DEF:filename:Module]
|
||||
|
||||
#### IV. КОНТРАКТ (DBC & UX)
|
||||
Расположение: Внутри [DEF], ПЕРЕД кодом.
|
||||
Стиль Python: Комментарии `# @TAG`.
|
||||
Стиль Svelte: JSDoc `/** @tag */` внутри `<script>`.
|
||||
## IV. КОНТРАКТЫ (DESIGN BY CONTRACT & UX)
|
||||
Контракты требуются адаптивно по уровню сложности, а не по жесткому tier.
|
||||
|
||||
**Базовые Теги:**
|
||||
@PURPOSE: Суть (High Entropy).
|
||||
@PRE: Входные условия.
|
||||
@POST: Гарантии выхода.
|
||||
@SIDE_EFFECT: Мутации, IO.
|
||||
|
||||
**UX Теги (Svelte/Frontend):**
|
||||
**@UX_STATE:** `[StateName] -> Визуальное поведение` (Idle, Loading, Error).
|
||||
**@UX_FEEDBACK:** Реакция системы (Toast, Shake, Red Border).
|
||||
**@UX_RECOVERY:** Механизм исправления ошибки пользователем (Retry, Clear Input).
|
||||
|
||||
**UX Testing Tags (для Tester Agent):**
|
||||
**@UX_TEST:** Спецификация теста для UX состояния.
|
||||
Формат: `@UX_TEST: [state] -> {action, expected}`
|
||||
Пример: `@UX_TEST: Idle -> {click: toggle, expected: isExpanded=true}`
|
||||
|
||||
Правило: Не используй `assert` в коде, используй `if/raise` или `guards`.
|
||||
**[CORE CONTRACTS]:**
|
||||
- `@PURPOSE:` Суть функции/компонента.
|
||||
- `@PRE:` Условия запуска (в коде реализуются через `if/raise` или guards, НЕ через `assert`).
|
||||
- `@POST:` Гарантии на выходе.
|
||||
- `@SIDE_EFFECT:` Мутации состояния, I/O, сеть.
|
||||
- `@DATA_CONTRACT:` Ссылка на DTO (Input -> Model, Output -> Model).
|
||||
|
||||
#### V. АДАПТАЦИЯ (TIERS)
|
||||
Определяется тегом `@TIER` в Header.
|
||||
**[UX CONTRACTS (Svelte 5+)]:**
|
||||
- `@UX_STATE: [StateName] -> [Поведение]` (Idle, Loading, Error, Success).
|
||||
- `@UX_FEEDBACK:` Реакция системы (Toast, Shake, RedBorder).
|
||||
- `@UX_RECOVERY:` Путь восстановления после сбоя (Retry, ClearInput).
|
||||
- `@UX_REACTIVITY:` Явный биндинг. *ЗАПРЕТ НА `$:` и `export let`. ТОЛЬКО Руны: `$state`, `$derived`, `$effect`, `$props`.*
|
||||
|
||||
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 модулей.
|
||||
2. **STANDARD** (BizLogic/**Forms**):
|
||||
- Требование: Базовый контракт (@PURPOSE, @UX_STATE), Логи, @RELATION.
|
||||
- @TEST_DATA: Рекомендуется для Complex Forms.
|
||||
3. **TRIVIAL** (DTO/**Atoms**):
|
||||
- Требование: Только Якоря [DEF] и @PURPOSE.
|
||||
**[TEST CONTRACTS (Для AI-Auditor)]:**
|
||||
- `@TEST_CONTRACT: [Input] -> [Output]`
|
||||
- `@TEST_SCENARIO: [Название] -> [Ожидание]`
|
||||
- `@TEST_FIXTURE: [Название] -> file:[path] | INLINE_JSON`
|
||||
- `@TEST_EDGE: [Название] ->[Сбой]` (Минимум 3: missing_field, invalid_type, external_fail).
|
||||
- `@TEST_INVARIANT: [Имя] -> VERIFIED_BY: [scenario_1, ...]`
|
||||
|
||||
#### VI. ЛОГИРОВАНИЕ (ДАО МОЛЕКУЛЫ / MOLECULAR TOPOLOGY)
|
||||
Цель: Трассировка. Самокоррекция. Управление Матрицей Внимания ("Химия мышления").
|
||||
Лог — не текст. Лог — реагент. Мысль облекается в форму через префиксы связи (Attention Energy):
|
||||
## V. ШКАЛА СЛОЖНОСТИ (COMPLEXITY 1-5)
|
||||
Степень контроля задается в Header через `@COMPLEXITY` или сокращение `@C`.
|
||||
Если тег отсутствует, сущность по умолчанию считается **Complexity 1**. Это сделано специально для экономии токенов и снижения шума на очевидных утилитах.
|
||||
|
||||
1. **[EXPLORE]** (Ван-дер-Ваальс: Рассеяние)
|
||||
- *Суть:* Поиск во тьме. Сплетение альтернатив. Если один путь закрыт — ищи иной.
|
||||
- *Время:* Фаза КАРКАС или столкновение с Неизведанным.
|
||||
- *Деяние:* `logger.explore("Основной API пал. Стучусь в запасной...")`
|
||||
- **1 — ATOMIC**
|
||||
- Примеры: DTO, исключения, геттеры, простые утилиты, короткие адаптеры.
|
||||
- Обязательны только якоря `[DEF]...[/DEF]`.
|
||||
- `@PURPOSE` желателен, но не обязателен.
|
||||
|
||||
2. **[REASON]** (Ковалентность: Твердость)
|
||||
- *Суть:* Жесткая нить дедукции. Шаг А неумолимо рождает Шаг Б. Контракт становится Кодом.
|
||||
- *Время:* Фаза РЕАЛИЗАЦИЯ. Прямота мысли.
|
||||
- *Деяние:* `logger.reason("Фундамент заложен. БД отвечает.")`
|
||||
- **2 — SIMPLE**
|
||||
- Примеры: простые helper-функции, небольшие мапперы, UI-атомы.
|
||||
- Обязателен `@PURPOSE`.
|
||||
- Остальные контракты опциональны.
|
||||
|
||||
3. **[REFLECT]** (Водород: Свертывание)
|
||||
- *Суть:* Взгляд назад. Сверка сущего (@POST) с ожидаемым (@PRE). Защита от бреда.
|
||||
- *Время:* Преддверие сложной логики и исход из неё.
|
||||
- *Деяние:* `logger.reflect("Вглядываюсь в кэш: нет ли там искомого?")`
|
||||
- **3 — FLOW**
|
||||
- Примеры: стандартная бизнес-логика, API handlers, сервисные методы, UI с загрузкой данных.
|
||||
- Обязательны: `@PURPOSE`, `@RELATION`.
|
||||
- Для UI дополнительно обязателен `@UX_STATE`.
|
||||
|
||||
4. **[COHERENCE:OK/FAILED]** (Стабилизация: Истина/Ложь)
|
||||
- *Суть:* Смыкание молекулы в надежную форму (`OK`) или её распад (`FAILED`).
|
||||
- *(Свершается незримо через `belief_scope` и печать `@believed`)*
|
||||
- **4 — ORCHESTRATION**
|
||||
- Примеры: сложная координация, работа с I/O, multi-step алгоритмы, stateful pipelines.
|
||||
- Обязательны: `@PURPOSE`, `@RELATION`, `@PRE`, `@POST`, `@SIDE_EFFECT`.
|
||||
- Для Python обязателен осмысленный путь логирования через `logger.reason()` / `logger.reflect()` или аналогичный belief-state механизм.
|
||||
|
||||
**Орудия Пути (`core.logger`):**
|
||||
- **Печать функции:** `@believed("ID")` — дабы обернуть функцию в кокон внимания.
|
||||
- **Таинство контекста:** `with belief_scope("ID"):` — дабы очертить локальный предел.
|
||||
- **Слова силы:** `logger.explore()`, `logger.reason()`, `logger.reflect()`.
|
||||
- **5 — CRITICAL**
|
||||
- Примеры: auth, security, database boundaries, migration core, money-like invariants.
|
||||
- Обязателен полный контракт: уровень 4 + `@DATA_CONTRACT` + `@INVARIANT`.
|
||||
- Для UI требуются UX-контракты.
|
||||
- Использование `belief_scope` строго обязательно.
|
||||
|
||||
**Незыблемое правило:** Всякому логу системы — тавро `source`. Для Внешенго Мира (Svelte) начертай рунами вручную: `console.log("[ID][REFLECT] Msg")`.
|
||||
**Legacy mapping (обратная совместимость):**
|
||||
- `@COMPLEXITY: 1` -> Complexity 1
|
||||
- `@COMPLEXITY: 3` -> Complexity 3
|
||||
- `@COMPLEXITY: 5` -> Complexity 5
|
||||
|
||||
#### VII. АЛГОРИТМ ГЕНЕРАЦИИ
|
||||
1. АНАЛИЗ. Оцени TIER, слой и UX-требования.
|
||||
2. КАРКАС. Создай `[DEF]`, Header и Контракты.
|
||||
3. РЕАЛИЗАЦИЯ. Напиши логику, удовлетворяющую Контракту (и UX-состояниям).
|
||||
4. ЗАМЫКАНИЕ. Закрой все `[/DEF]`.
|
||||
## VI. ПРОТОКОЛ ЛОГИРОВАНИЯ (THREAD-LOCAL BELIEF STATE)
|
||||
Логирование — это механизм трассировки рассуждений ИИ (CoT) и управления Attention Energy. Архитектура использует Thread-local storage (`_belief_state`), поэтому `ID` прокидывается автоматически.
|
||||
|
||||
ЕСЛИ ошибка или противоречие -> СТОП. Выведи `[COHERENCE_CHECK_FAILED]`.
|
||||
**[PYTHON CORE TOOLS]:**
|
||||
Импорт: `from ...logger import logger, belief_scope, believed`
|
||||
1. **Декоратор:** `@believed("ID")` — автоматический трекинг функции.
|
||||
2. **Контекст:** `with belief_scope("ID"):` — очерчивает локальный предел мысли. НЕ возвращает context, используется просто как `with`.
|
||||
3. **Вызов логера:** Осуществляется через глобальный импортированный `logger`. Дополнительные данные передавать через `extra={...}`.
|
||||
|
||||
**[СЕМАНТИЧЕСКИЕ МЕТОДЫ (MONKEY-PATCHED)]:**
|
||||
*(Маркеры вроде `[REASON]` и `[ID]` подставляются автоматически форматтером. Не пиши их в тексте!)*
|
||||
1. **`logger.explore(msg, extra={...})`** (Поиск/Ветвление): Применяется при фолбэках, `except`, проверке гипотез. Эмитирует WARNING.
|
||||
*Пример:* `logger.explore("Insufficient funds", extra={"balance": bal})`
|
||||
2. **`logger.reason(msg, extra={...})`** (Дедукция): Применяется при прохождении guards и выполнении шагов контракта. Эмитирует INFO.
|
||||
*Пример:* `logger.reason("Initiating transfer")`
|
||||
3. **`logger.reflect(msg, extra={...})`** (Самопроверка): Применяется для сверки результата с `@POST` перед `return`. Эмитирует DEBUG.
|
||||
*Пример:* `logger.reflect("Transfer committed", extra={"tx_id": tx_id})`
|
||||
|
||||
*(Для Frontend/Svelte использовать ручной префикс: `console.info("[ID][REFLECT] Text", {data})`)*
|
||||
|
||||
## VII. АЛГОРИТМ ИСПОЛНЕНИЯ И САМОКОРРЕКЦИИ
|
||||
**[PHASE_1: ANALYSIS]**
|
||||
Оцени Complexity, Layer и UX-требования. При слепоте контекста -> `yield [NEED_CONTEXT: id]`.
|
||||
**[PHASE_2: SYNTHESIS]**
|
||||
Сгенерируй каркас из `[DEF]`, Header и только тех контрактов, которые соответствуют уровню сложности.
|
||||
**[PHASE_3: IMPLEMENTATION]**
|
||||
Напиши код строго по Контракту. Для Complexity 5 секций открой `with belief_scope("ID"):` и орошай путь вызовами `logger.reason()` и `logger.reflect()`.
|
||||
**[PHASE_4: CLOSURE]**
|
||||
Убедись, что все `[DEF]` закрыты соответствующими `[/DEF]`.
|
||||
|
||||
**[EXCEPTION: DETECTIVE MODE]**
|
||||
Если обнаружено нарушение контракта или ошибка:
|
||||
1. СТОП-СИГНАЛ: Выведи `[COHERENCE_CHECK_FAILED]`.
|
||||
2. ГИПОТЕЗА: Сгенерируй вызов `logger.explore("Ошибка в I/O / Состоянии / Зависимости -> Описание")`.
|
||||
3. ЗАПРОС: Запроси разрешение на изменение контракта.
|
||||
|
||||
## VIII. ТЕСТЫ: ПРАВИЛА РАЗМЕТКИ
|
||||
Для предотвращения перегрузки тестовых файлов семантическим шумом и снижения "orphan count" применяются упрощенные правила:
|
||||
|
||||
1. **Короткие ID:** Тестовые модули ОБЯЗАНЫ иметь короткие семантические ID (например, `AssistantApiTests`), а не полные пути импорта.
|
||||
2. **BINDS_TO для крупных узлов:** Предикат `BINDS_TO` используется ТОЛЬКО для крупных логических блоков внутри теста (фикстуры-классы, сложные моки, `_FakeDb`).
|
||||
3. **Complexity 1 для хелперов:** Мелкие вспомогательные функции внутри теста (`_run_async`, `_setup_mock`) остаются на уровне Complexity 1. Для них `@RELATION` и `@PURPOSE` не требуются — достаточно якорей `[DEF]...[/DEF]`.
|
||||
4. **Тестовые сценарии:** Сами функции тестов (`test_...`) по умолчанию считаются Complexity 2 (требуется только `@PURPOSE`). Использование `BINDS_TO` для них опционально.
|
||||
5. **Запрет на цепочки:** Не нужно описывать граф вызовов внутри теста. Достаточно "заземлить" 1-2 главных хелпера на ID модуля через `BINDS_TO`, чтобы файл перестал считаться набором сирот.
|
||||
1587
.ai/structure/MODULE_MAP.md
Normal file
1587
.ai/structure/MODULE_MAP.md
Normal file
File diff suppressed because it is too large
Load Diff
4299
.ai/structure/PROJECT_MAP.md
Normal file
4299
.ai/structure/PROJECT_MAP.md
Normal file
File diff suppressed because it is too large
Load Diff
103
.codex/prompts/audit-test.md
Normal file
103
.codex/prompts/audit-test.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
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_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT`).
|
||||
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_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT` 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).
|
||||
|
||||
5. **Missing Edge Case Coverage:**
|
||||
- *Definition:* The test suite ignores `@TEST_EDGE` scenarios defined in the contract.
|
||||
- *Rule:* Every `@TEST_EDGE` in the source contract MUST have a corresponding test case.
|
||||
|
||||
6. **Missing Invariant Verification:**
|
||||
- *Definition:* The test suite does not verify `@TEST_INVARIANT` conditions.
|
||||
- *Rule:* Every `@TEST_INVARIANT` MUST be verified by at least one test that attempts to break it.
|
||||
|
||||
7. **Missing UX State Testing (Svelte Components):**
|
||||
- *Definition:* For Svelte components with `@UX_STATE`, the test suite does not verify state transitions.
|
||||
- *Rule:* Every `@UX_STATE` transition MUST have a test verifying the visual/behavioral change.
|
||||
- *Check:* `@UX_FEEDBACK` mechanisms (toast, shake, color) must be tested.
|
||||
- *Check:* `@UX_RECOVERY` mechanisms (retry, clear input) must be tested.
|
||||
|
||||
### II. SEMANTIC PROTOCOL COMPLIANCE
|
||||
|
||||
Verify the test file follows GRACE-Poly semantics:
|
||||
|
||||
1. **Anchor Integrity:**
|
||||
- Test file MUST start with `[DEF:__tests__/test_name:Module]`
|
||||
- Test file MUST end with `[/DEF:__tests__/test_name:Module]`
|
||||
|
||||
2. **Required Tags:**
|
||||
- `@RELATION: VERIFIES -> <path_to_source>` must be present
|
||||
- `@PURPOSE:` must describe what is being tested
|
||||
|
||||
3. **TIER Alignment:**
|
||||
- If source is `@TIER: CRITICAL`, test MUST cover all `@TEST_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT`
|
||||
- If source is `@TIER: STANDARD`, test MUST cover `@PRE` and `@POST`
|
||||
- If source is `@TIER: TRIVIAL`, basic smoke test is acceptable
|
||||
|
||||
### III. 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. **Test Contract Compliance:** Does the test follow the interface defined in `@TEST_CONTRACT`?
|
||||
4. **Data Usage:** Does the test use the exact scenarios defined in `@TEST_FIXTURE`?
|
||||
5. **Edge Coverage:** Are all `@TEST_EDGE` scenarios tested?
|
||||
6. **Invariant Coverage:** Are all `@TEST_INVARIANT` conditions verified?
|
||||
7. **UX Coverage (if applicable):** Are all `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY` tested?
|
||||
8. **Mocking Sanity:** Are external dependencies mocked correctly WITHOUT mocking the system under test itself?
|
||||
9. **Semantic Anchor:** Does the test file have proper `[DEF]` and `[/DEF]` anchors?
|
||||
|
||||
### IV. 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" | "MISSING_EDGES" | "MISSING_INVARIANTS" | "MISSING_UX_TESTS" | "SEMANTIC_VIOLATION" | "NONE",
|
||||
"audit_details": {
|
||||
"target_invoked": true/false,
|
||||
"pre_conditions_tested": true/false,
|
||||
"post_conditions_tested": true/false,
|
||||
"test_fixture_used": true/false,
|
||||
"edges_covered": true/false,
|
||||
"invariants_verified": true/false,
|
||||
"ux_states_tested": true/false,
|
||||
"semantic_anchors_present": true/false
|
||||
},
|
||||
"coverage_summary": {
|
||||
"total_edges": number,
|
||||
"edges_tested": number,
|
||||
"total_invariants": number,
|
||||
"invariants_tested": number,
|
||||
"total_ux_states": number,
|
||||
"ux_states_tested": number
|
||||
},
|
||||
"tier_compliance": {
|
||||
"source_tier": "CRITICAL" | "STANDARD" | "TRIVIAL",
|
||||
"meets_tier_requirements": 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
|
||||
---
|
||||
Прочитай .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.
|
||||
185
.codex/prompts/speckit.analyze.md
Normal file
185
.codex/prompts/speckit.analyze.md
Normal file
@@ -0,0 +1,185 @@
|
||||
---
|
||||
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
|
||||
- Load `.ai/standards/semantics.md` for technical standard 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_ 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
|
||||
150
.codex/prompts/speckit.implement.md
Normal file
150
.codex/prompts/speckit.implement.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
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
|
||||
|
||||
3. Load and analyze the implementation context:
|
||||
- **REQUIRED**: Read `.ai/standards/semantics.md` for strict coding standards and contract requirements
|
||||
- **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:
|
||||
- **Strict Adherence**: Apply `.ai/standards/semantics.md` rules:
|
||||
- Every file MUST start with a `[DEF:id:Type]` header and end with a closing `[/DEF:id:Type]` anchor.
|
||||
- Include `@TIER` and define contracts (`@PRE`, `@POST`).
|
||||
- For Svelte components, use `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY`, and explicitly declare reactivity with `@UX_REATIVITY: State: $state, Derived: $derived`.
|
||||
- **Molecular Topology Logging**: Use prefixes `[EXPLORE]`, `[REASON]`, `[REFLECT]` in logs to trace logic.
|
||||
- **CRITICAL Contracts**: If a task description contains a contract summary (e.g., `CRITICAL: PRE: ..., POST: ...`), these constraints are **MANDATORY** and must be strictly implemented in the code using guards/assertions (if applicable per protocol).
|
||||
- **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.
|
||||
104
.codex/prompts/speckit.plan.md
Normal file
104
.codex/prompts/speckit.plan.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
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 `.ai/ROOT.md` and `.ai/PROJECT_MAP.md` to understand the project structure and navigation. Then read required standards: `.ai/standards/constitution.md` and `.ai/standards/semantics.md`. Load IMPL_PLAN template.
|
||||
|
||||
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
|
||||
|
||||
0. **Validate Design against UX Reference**:
|
||||
- Check if the proposed architecture supports the latency, interactivity, and flow defined in `ux_reference.md`.
|
||||
- **Linkage**: Ensure key UI states from `ux_reference.md` map to Component Contracts (`@UX_STATE`).
|
||||
- **CRITICAL**: If the technical plan compromises the UX (e.g. "We can't do real-time validation"), you **MUST STOP** and warn the user.
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships, validation rules.
|
||||
|
||||
2. **Design & Verify Contracts (Semantic Protocol)**:
|
||||
- **Drafting**: Define `[DEF:id:Type]` Headers, Contracts, and closing `[/DEF:id:Type]` for all new modules based on `.ai/standards/semantics.md`.
|
||||
- **TIER Classification**: Explicitly assign `@TIER: [CRITICAL|STANDARD|TRIVIAL]` to each module.
|
||||
- **CRITICAL Requirements**: For all CRITICAL modules, define full `@PRE`, `@POST`, and (if UI) `@UX_STATE` contracts. **MUST** also define testing contracts: `@TEST_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, and `@TEST_INVARIANT`.
|
||||
- **Self-Review**:
|
||||
- *Completeness*: Do `@PRE`/`@POST` cover edge cases identified in Research? Are test contracts present for CRITICAL?
|
||||
- *Connectivity*: Do `@RELATION` tags form a coherent graph?
|
||||
- *Compliance*: Does syntax match `[DEF:id:Type]` exactly and is it closed with `[/DEF:id:Type]`?
|
||||
- **Output**: Write verified contracts to `contracts/modules.md`.
|
||||
|
||||
3. **Simulate Contract Usage**:
|
||||
- Trace one key user scenario through the defined contracts to ensure data flow continuity.
|
||||
- If a contract interface mismatch is found, fix it immediately.
|
||||
|
||||
4. **Generate API contracts**:
|
||||
- Output OpenAPI/GraphQL schema to `/contracts/` for backend-frontend sync.
|
||||
|
||||
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)
|
||||
146
.codex/prompts/speckit.tasks.md
Normal file
146
.codex/prompts/speckit.tasks.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
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), ux_reference.md (experience source of truth)
|
||||
- **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.
|
||||
|
||||
### UX Preservation (CRITICAL)
|
||||
|
||||
- **Source of Truth**: `ux_reference.md` is the absolute standard for the "feel" of the feature.
|
||||
- **Violation Warning**: If any task would inherently violate the UX (e.g. "Remove progress bar to simplify code"), you **MUST** flag this to the user immediately.
|
||||
- **Verification Task**: You **MUST** add a specific task at the end of each User Story phase: `- [ ] Txxx [USx] Verify implementation matches ux_reference.md (Happy Path & Errors)`
|
||||
|
||||
### 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 (CRITICAL TIER)**:
|
||||
- Identify components marked as `@TIER: CRITICAL` in `contracts/modules.md`.
|
||||
- For these components, **MUST** append the summary of `@PRE`, `@POST`, `@UX_STATE`, and test contracts (`@TEST_FIXTURE`, `@TEST_EDGE`) directly to the task description.
|
||||
- Example: `- [ ] T005 [P] [US1] Implement Auth (CRITICAL: PRE: token exists, POST: returns User, TESTS: 2 edges) in src/auth.py`
|
||||
- Map each contract/endpoint → to the user story it serves
|
||||
- If tests requested: Each 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
|
||||
179
.codex/prompts/speckit.test.md
Normal file
179
.codex/prompts/speckit.test.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
|
||||
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_FIXTURE fixtures** - For CRITICAL tier modules, read @TEST_FIXTURE from semantics header
|
||||
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 .ai/standards/semantics.md:**
|
||||
- Read @TIER annotations for modules
|
||||
- For CRITICAL modules: Read @TEST_ 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_FIXTURE Available |
|
||||
|--------|------|-----------|------|----------------------|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
### 4. Write Tests (TDD Approach)
|
||||
|
||||
For each module requiring tests:
|
||||
|
||||
1. **Check existing tests**: Scan `__tests__/` for duplicates
|
||||
2. **Read TEST_FIXTURE**: If CRITICAL tier, read @TEST_FIXTURE from semantic header
|
||||
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 () => { ... });
|
||||
});
|
||||
// [/DEF:__tests__/test_Component:Module]
|
||||
```
|
||||
|
||||
### 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_FIXTURE fixtures
|
||||
```
|
||||
|
||||
## Context for Testing
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -6,6 +6,8 @@
|
||||
.ai
|
||||
.specify
|
||||
.kilocode
|
||||
.codex
|
||||
.agent
|
||||
venv
|
||||
backend/.venv
|
||||
backend/.pytest_cache
|
||||
|
||||
27
.env.enterprise-clean.example
Normal file
27
.env.enterprise-clean.example
Normal file
@@ -0,0 +1,27 @@
|
||||
# Offline / air-gapped compose profile for enterprise clean release.
|
||||
|
||||
BACKEND_IMAGE=ss-tools-backend:v1.0.0-rc2-docker
|
||||
FRONTEND_IMAGE=ss-tools-frontend:v1.0.0-rc2-docker
|
||||
POSTGRES_IMAGE=postgres:16-alpine
|
||||
|
||||
POSTGRES_DB=ss_tools
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=change-me
|
||||
|
||||
BACKEND_HOST_PORT=8001
|
||||
FRONTEND_HOST_PORT=8000
|
||||
POSTGRES_HOST_PORT=5432
|
||||
|
||||
ENABLE_BELIEF_STATE_LOGGING=true
|
||||
TASK_LOG_LEVEL=INFO
|
||||
|
||||
STORAGE_ROOT=./storage
|
||||
|
||||
# Initial admin bootstrap. Set to true only for the first startup in a new environment.
|
||||
INITIAL_ADMIN_CREATE=false
|
||||
INITIAL_ADMIN_USERNAME=admin
|
||||
INITIAL_ADMIN_PASSWORD=change-me
|
||||
INITIAL_ADMIN_EMAIL=
|
||||
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
21
.gitattributes
vendored
Normal file
21
.gitattributes
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.pdf binary
|
||||
*.zip binary
|
||||
*.gz binary
|
||||
*.tar binary
|
||||
*.db binary
|
||||
*.sqlite binary
|
||||
*.p12 binary
|
||||
*.pfx binary
|
||||
*.crt binary
|
||||
*.pem binary
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -65,13 +65,15 @@ backend/mappings.db
|
||||
|
||||
|
||||
backend/tasks.db
|
||||
backend/logs
|
||||
backend/auth.db
|
||||
semantics/reports
|
||||
backend/tasks.db
|
||||
|
||||
# Universal / tooling
|
||||
node_modules/
|
||||
backend/logs
|
||||
backend/auth.db
|
||||
semantics/reports
|
||||
backend/tasks.db
|
||||
backend/**/*.db
|
||||
backend/**/*.sqlite
|
||||
|
||||
# Universal / tooling
|
||||
node_modules/
|
||||
.venv/
|
||||
coverage/
|
||||
*.tmp
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"mcpServers":{}}
|
||||
{"mcpServers":{"axiom-core":{"command":"/home/busya/dev/ast-mcp-core-server/.venv/bin/python","args":["-c","from src.server import main; main()"],"env":{"PYTHONPATH":"/home/busya/dev/ast-mcp-core-server"},"alwaysAllow":["read_grace_outline_tool","ast_search_tool","get_semantic_context_tool","build_task_context_tool","audit_contracts_tool","diff_contract_semantics_tool","simulate_patch_tool","patch_contract_tool","rename_contract_id_tool","move_contract_tool","extract_contract_tool","infer_missing_relations_tool","map_runtime_trace_to_contracts_tool","scaffold_contract_tests_tool","search_contracts_tool","reindex_workspace_tool","prune_contract_metadata_tool","workspace_semantic_health_tool","trace_tests_for_contract_tool"]}}}
|
||||
@@ -45,6 +45,12 @@ Auto-generated from all feature plans. Last updated: 2025-12-19
|
||||
- SQLite task/result persistence (existing task DB), filesystem only for existing artifacts (no new primary store required) (020-task-reports-design)
|
||||
- Node.js 18+ runtime, SvelteKit (existing frontend stack) + SvelteKit, Tailwind CSS, existing frontend UI primitives under `frontend/src/lib/components/ui` (001-unify-frontend-style)
|
||||
- N/A (UI styling and component behavior only) (001-unify-frontend-style)
|
||||
- Python 3.9+ (backend scripts/services), Shell (release tooling) + FastAPI stack (existing backend), ConfigManager, TaskManager, файловые утилиты, internal artifact registries (020-clean-repo-enterprise)
|
||||
- PostgreSQL (конфигурации/метаданные), filesystem (артефакты дистрибутива, отчёты проверки) (020-clean-repo-enterprise)
|
||||
- Python 3.9+ (backend), Node.js 18+ + SvelteKit (frontend) + FastAPI, SQLAlchemy, Pydantic, existing auth stack (`get_current_user`), existing dashboards route/service, Svelte runes (`$state`, `$derived`, `$effect`), Tailwind CSS, frontend `api` wrapper (024-user-dashboard-filter)
|
||||
- Existing auth database (`AUTH_DATABASE_URL`) with a dedicated per-user preference entity (024-user-dashboard-filter)
|
||||
- Python 3.9+ (Backend), Node.js 18+ / Svelte 5.x (Frontend) + FastAPI, SQLAlchemy, APScheduler (Backend) | SvelteKit, Tailwind CSS, existing UI components (Frontend) (026-dashboard-health-windows)
|
||||
- PostgreSQL / SQLite (existing database for `ValidationRecord` and new `ValidationPolicy`) (026-dashboard-health-windows)
|
||||
|
||||
- Python 3.9+ (Backend), Node.js 18+ (Frontend Build) (001-plugin-arch-svelte-ui)
|
||||
|
||||
@@ -65,9 +71,9 @@ cd src; pytest; ruff check .
|
||||
Python 3.9+ (Backend), Node.js 18+ (Frontend Build): Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
- 001-unify-frontend-style: Added Node.js 18+ runtime, SvelteKit (existing frontend stack) + SvelteKit, Tailwind CSS, existing frontend UI primitives under `frontend/src/lib/components/ui`
|
||||
- 020-task-reports-design: Added Python 3.9+ (backend), Node.js 18+ (frontend) + FastAPI, SvelteKit, Tailwind CSS, SQLAlchemy/Pydantic task models, existing task/websocket stack
|
||||
- 019-superset-ux-redesign: Added Python 3.9+ (Backend), Node.js 18+ (Frontend) + FastAPI, SvelteKit, Tailwind CSS, SQLAlchemy, WebSocket (existing)
|
||||
- 026-dashboard-health-windows: Added Python 3.9+ (Backend), Node.js 18+ / Svelte 5.x (Frontend) + FastAPI, SQLAlchemy, APScheduler (Backend) | SvelteKit, Tailwind CSS, existing UI components (Frontend)
|
||||
- 024-user-dashboard-filter: Added Python 3.9+ (backend), Node.js 18+ + SvelteKit (frontend) + FastAPI, SQLAlchemy, Pydantic, existing auth stack (`get_current_user`), existing dashboards route/service, Svelte runes (`$state`, `$derived`, `$effect`), Tailwind CSS, frontend `api` wrapper
|
||||
- 020-clean-repo-enterprise: Added Python 3.9+ (backend scripts/services), Shell (release tooling) + FastAPI stack (existing backend), ConfigManager, TaskManager, файловые утилиты, internal artifact registries
|
||||
|
||||
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
|
||||
39
.kilocode/setup-script
Executable file
39
.kilocode/setup-script
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Kilo Code Worktree Setup Script
|
||||
# This script runs before the agent starts in a worktree (new sessions only).
|
||||
#
|
||||
# Available environment variables:
|
||||
# WORKTREE_PATH - Absolute path to the worktree directory
|
||||
# REPO_PATH - Absolute path to the main repository
|
||||
#
|
||||
# Example tasks:
|
||||
# - Copy .env files from main repo
|
||||
# - Install dependencies
|
||||
# - Run database migrations
|
||||
# - Set up local configuration
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
echo "Setting up worktree: $WORKTREE_PATH"
|
||||
|
||||
# Uncomment and modify as needed:
|
||||
|
||||
# Copy environment files
|
||||
# if [ -f "$REPO_PATH/.env" ]; then
|
||||
# cp "$REPO_PATH/.env" "$WORKTREE_PATH/.env"
|
||||
# echo "Copied .env"
|
||||
# fi
|
||||
|
||||
# Install dependencies (Node.js)
|
||||
# if [ -f "$WORKTREE_PATH/package.json" ]; then
|
||||
# cd "$WORKTREE_PATH"
|
||||
# npm install
|
||||
# fi
|
||||
|
||||
# Install dependencies (Python)
|
||||
# if [ -f "$WORKTREE_PATH/requirements.txt" ]; then
|
||||
# cd "$WORKTREE_PATH"
|
||||
# pip install -r requirements.txt
|
||||
# fi
|
||||
|
||||
echo "Setup complete!"
|
||||
103
.kilocode/workflows/audit-test.md
Normal file
103
.kilocode/workflows/audit-test.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
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_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT`).
|
||||
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_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT` 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).
|
||||
|
||||
5. **Missing Edge Case Coverage:**
|
||||
- *Definition:* The test suite ignores `@TEST_EDGE` scenarios defined in the contract.
|
||||
- *Rule:* Every `@TEST_EDGE` in the source contract MUST have a corresponding test case.
|
||||
|
||||
6. **Missing Invariant Verification:**
|
||||
- *Definition:* The test suite does not verify `@TEST_INVARIANT` conditions.
|
||||
- *Rule:* Every `@TEST_INVARIANT` MUST be verified by at least one test that attempts to break it.
|
||||
|
||||
7. **Missing UX State Testing (Svelte Components):**
|
||||
- *Definition:* For Svelte components with `@UX_STATE`, the test suite does not verify state transitions.
|
||||
- *Rule:* Every `@UX_STATE` transition MUST have a test verifying the visual/behavioral change.
|
||||
- *Check:* `@UX_FEEDBACK` mechanisms (toast, shake, color) must be tested.
|
||||
- *Check:* `@UX_RECOVERY` mechanisms (retry, clear input) must be tested.
|
||||
|
||||
### II. SEMANTIC PROTOCOL COMPLIANCE
|
||||
|
||||
Verify the test file follows GRACE-Poly semantics:
|
||||
|
||||
1. **Anchor Integrity:**
|
||||
- Test file MUST start with a short semantic ID (e.g., `[DEF:AuthTests:Module]`), NOT a file path.
|
||||
- Test file MUST end with a matching `[/DEF]` anchor.
|
||||
|
||||
2. **Required Tags:**
|
||||
- `@RELATION: VERIFIES -> <path_to_source>` must be present
|
||||
- `@PURPOSE:` must describe what is being tested
|
||||
|
||||
3. **TIER Alignment:**
|
||||
- If source is `@TIER: CRITICAL`, test MUST cover all `@TEST_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, `@TEST_INVARIANT`
|
||||
- If source is `@TIER: STANDARD`, test MUST cover `@PRE` and `@POST`
|
||||
- If source is `@TIER: TRIVIAL`, basic smoke test is acceptable
|
||||
|
||||
### III. 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. **Test Contract Compliance:** Does the test follow the interface defined in `@TEST_CONTRACT`?
|
||||
4. **Data Usage:** Does the test use the exact scenarios defined in `@TEST_FIXTURE`?
|
||||
5. **Edge Coverage:** Are all `@TEST_EDGE` scenarios tested?
|
||||
6. **Invariant Coverage:** Are all `@TEST_INVARIANT` conditions verified?
|
||||
7. **UX Coverage (if applicable):** Are all `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY` tested?
|
||||
8. **Mocking Sanity:** Are external dependencies mocked correctly WITHOUT mocking the system under test itself?
|
||||
9. **Semantic Anchor:** Does the test file have proper `[DEF]` and `[/DEF]` anchors?
|
||||
|
||||
### IV. 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" | "MISSING_EDGES" | "MISSING_INVARIANTS" | "MISSING_UX_TESTS" | "SEMANTIC_VIOLATION" | "NONE",
|
||||
"audit_details": {
|
||||
"target_invoked": true/false,
|
||||
"pre_conditions_tested": true/false,
|
||||
"post_conditions_tested": true/false,
|
||||
"test_fixture_used": true/false,
|
||||
"edges_covered": true/false,
|
||||
"invariants_verified": true/false,
|
||||
"ux_states_tested": true/false,
|
||||
"semantic_anchors_present": true/false
|
||||
},
|
||||
"coverage_summary": {
|
||||
"total_edges": number,
|
||||
"edges_tested": number,
|
||||
"total_invariants": number,
|
||||
"invariants_tested": number,
|
||||
"total_ux_states": number,
|
||||
"ux_states_tested": number
|
||||
},
|
||||
"tier_compliance": {
|
||||
"source_tier": "CRITICAL" | "STANDARD" | "TRIVIAL",
|
||||
"meets_tier_requirements": true/false
|
||||
},
|
||||
"feedback": "Strict, actionable feedback for the test generator agent. Explain exactly which anti-pattern was detected and how to fix it."
|
||||
}
|
||||
@@ -20,7 +20,7 @@ Analyze test failure reports, identify root causes, and fix implementation issue
|
||||
|
||||
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
|
||||
3. **TEST DATA**: If tests use @TEST_ 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
|
||||
|
||||
|
||||
@@ -117,7 +117,11 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
- **Validation checkpoints**: Verify each phase completion before proceeding
|
||||
|
||||
7. Implementation execution rules:
|
||||
- **Strict Adherence**: Apply `.ai/standards/semantics.md` rules - every file must start with [DEF] header, include @TIER, and define contracts.
|
||||
- **Strict Adherence**: Apply `.ai/standards/semantics.md` rules:
|
||||
- Every file MUST start with a `[DEF:id:Type]` header and end with a closing `[/DEF:id:Type]` anchor.
|
||||
- Include `@TIER` and define contracts (`@PRE`, `@POST`).
|
||||
- For Svelte components, use `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY`, and explicitly declare reactivity with `@UX_REATIVITY: State: $state, Derived: $derived`.
|
||||
- **Molecular Topology Logging**: Use prefixes `[EXPLORE]`, `[REASON]`, `[REFLECT]` in logs to trace logic.
|
||||
- **CRITICAL Contracts**: If a task description contains a contract summary (e.g., `CRITICAL: PRE: ..., POST: ...`), these constraints are **MANDATORY** and must be strictly implemented in the code using guards/assertions (if applicable per protocol).
|
||||
- **Setup first**: Initialize project structure, dependencies, configuration
|
||||
- **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios
|
||||
|
||||
@@ -73,13 +73,13 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
- Entity name, fields, relationships, validation rules.
|
||||
|
||||
2. **Design & Verify Contracts (Semantic Protocol)**:
|
||||
- **Drafting**: Define [DEF] Headers and Contracts for all new modules based on `.ai/standards/semantics.md`.
|
||||
- **Drafting**: Define `[DEF:id:Type]` Headers, Contracts, and closing `[/DEF:id:Type]` for all new modules based on `.ai/standards/semantics.md`.
|
||||
- **TIER Classification**: Explicitly assign `@TIER: [CRITICAL|STANDARD|TRIVIAL]` to each module.
|
||||
- **CRITICAL Requirements**: For all CRITICAL modules, define full `@PRE`, `@POST`, and (if UI) `@UX_STATE` contracts.
|
||||
- **CRITICAL Requirements**: For all CRITICAL modules, define full `@PRE`, `@POST`, and (if UI) `@UX_STATE` contracts. **MUST** also define testing contracts: `@TEST_CONTRACT`, `@TEST_FIXTURE`, `@TEST_EDGE`, and `@TEST_INVARIANT`.
|
||||
- **Self-Review**:
|
||||
- *Completeness*: Do `@PRE`/`@POST` cover edge cases identified in Research?
|
||||
- *Completeness*: Do `@PRE`/`@POST` cover edge cases identified in Research? Are test contracts present for CRITICAL?
|
||||
- *Connectivity*: Do `@RELATION` tags form a coherent graph?
|
||||
- *Compliance*: Does syntax match `[DEF:id:Type]` exactly?
|
||||
- *Compliance*: Does syntax match `[DEF:id:Type]` exactly and is it closed with `[/DEF:id:Type]`?
|
||||
- **Output**: Write verified contracts to `contracts/modules.md`.
|
||||
|
||||
3. **Simulate Contract Usage**:
|
||||
|
||||
83
.kilocode/workflows/speckit.semantics.md
Normal file
83
.kilocode/workflows/speckit.semantics.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
description: Maintain semantic integrity by generating maps and auditing compliance reports.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Goal
|
||||
|
||||
Ensure the codebase adheres to the semantic standards defined in `.ai/standards/semantics.md` by using the AXIOM MCP semantic graph as the primary execution engine. This involves reindexing the workspace, measuring semantic health, auditing contract compliance, and optionally delegating contract-safe fixes through MCP-aware agents.
|
||||
|
||||
## Operating Constraints
|
||||
|
||||
1. **ROLE: Orchestrator**: You are responsible for the high-level coordination of semantic maintenance.
|
||||
2. **MCP-FIRST**: Use the connected AXIOM MCP server as the default mechanism for discovery, health checks, audit, semantic context, impact analysis, and contract mutation planning.
|
||||
3. **STRICT ADHERENCE**: Follow `.ai/standards/semantics.md` for all anchor and tag syntax.
|
||||
4. **NON-DESTRUCTIVE**: Do not remove existing code logic; only add or update semantic annotations.
|
||||
5. **TIER AWARENESS**: Prioritize CRITICAL and STANDARD modules for compliance fixes.
|
||||
6. **NO PSEUDO-CONTRACTS (CRITICAL)**: You are STRICTLY FORBIDDEN from using automated scripts (e.g., Python/Bash/sed) to mechanically inject boilerplate, placeholders, or "pseudo-contracts" merely to artificially inflate the compliance score. Every semantic tag, anchor, and contract you add MUST reflect a genuine, deep understanding of the code's actual logic and business requirements.
|
||||
7. **ID NAMING (CRITICAL)**: NEVER use fully-qualified Python import paths in `[DEF:id:Type]`. Use short, domain-driven semantic IDs (e.g., `[DEF:AuthService:Class]`). Follow the exact style shown in `.ai/standards/semantics.md`.
|
||||
8. **ORPHAN PREVENTION**: To reduce the orphan count, you MUST physically wrap actual class and function definitions with `[DEF:id:Type] ... [/DEF]` blocks in the code. Modifying `@RELATION` tags does NOT fix orphans. The AST parser flags any unwrapped function as an orphan.
|
||||
- **Exception for Tests**: In test modules, use `BINDS_TO` to link major helpers to the module root. Small helpers remain C1 and don't need relations.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Reindex Semantic Workspace
|
||||
|
||||
Use MCP to refresh the semantic graph for the current workspace with [`reindex_workspace_tool`](.kilocode/mcp.json).
|
||||
|
||||
### 2. Analyze Semantic Health
|
||||
|
||||
Use [`workspace_semantic_health_tool`](.kilocode/mcp.json) and capture:
|
||||
- `contracts`
|
||||
- `relations`
|
||||
- `orphans`
|
||||
- `unresolved_relations`
|
||||
- `files`
|
||||
|
||||
Treat high orphan counts and unresolved relations as first-class health indicators, not just informational noise.
|
||||
|
||||
### 3. Audit Critical Issues
|
||||
|
||||
Use [`audit_contracts_tool`](.kilocode/mcp.json) and classify findings into:
|
||||
- **Critical Parsing/Structure Errors**: malformed or incoherent semantic contract regions
|
||||
- **Critical Contract Gaps**: missing [`@DATA_CONTRACT`](.ai/standards/semantics.md), [`@PRE`](.ai/standards/semantics.md), [`@POST`](.ai/standards/semantics.md), [`@SIDE_EFFECT`](.ai/standards/semantics.md) on CRITICAL contracts
|
||||
- **Coverage Gaps**: missing [`@TIER`](.ai/standards/semantics.md), missing [`@PURPOSE`](.ai/standards/semantics.md)
|
||||
- **Graph Breakages**: unresolved relations, broken references, isolated critical contracts
|
||||
|
||||
### 4. Build Remediation Context
|
||||
|
||||
For the top failing contracts, use MCP semantic context tools such as [`get_semantic_context_tool`](.kilocode/mcp.json), [`build_task_context_tool`](.kilocode/mcp.json), [`impact_analysis_tool`](.kilocode/mcp.json), and [`trace_tests_for_contract_tool`](.kilocode/mcp.json) to understand:
|
||||
1. Local contract intent
|
||||
2. Upstream/downstream semantic impact
|
||||
3. Related tests and fixtures
|
||||
4. Whether relation recovery is needed
|
||||
|
||||
### 5. Execute Fixes (Optional/Handoff)
|
||||
|
||||
If $ARGUMENTS contains `fix` or `apply`:
|
||||
- Handoff to the [`semantic`](.kilocodemodes) mode or a dedicated implementation agent instead of applying naive textual edits in orchestration.
|
||||
- Require the fixing agent to prefer MCP contract mutation tools such as [`simulate_patch_tool`](.kilocode/mcp.json), [`guarded_patch_contract_tool`](.kilocode/mcp.json), [`patch_contract_tool`](.kilocode/mcp.json), and [`infer_missing_relations_tool`](.kilocode/mcp.json).
|
||||
- After changes, re-run reindex, health, and audit MCP steps to verify the delta.
|
||||
|
||||
### 6. Review Gate
|
||||
|
||||
Before completion, request or perform an MCP-based review path aligned with the [`reviewer-agent-auditor`](.kilocodemodes) mode so the workflow produces a semantic PASS/FAIL gate, not just a remediation list.
|
||||
|
||||
## Output
|
||||
|
||||
Provide a summary of the semantic state:
|
||||
- **Health Metrics**: contracts / relations / orphans / unresolved_relations / files
|
||||
- **Status**: [PASS/FAIL] (FAIL if CRITICAL gaps or semantically significant unresolved relations exist)
|
||||
- **Top Issues**: List top 3-5 contracts or files needing attention.
|
||||
- **Action Taken**: Summary of MCP analysis performed, context gathered, and fixes or handoffs initiated.
|
||||
|
||||
## Context
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -121,8 +121,8 @@ Every task MUST strictly follow this format:
|
||||
|
||||
2. **From Contracts (CRITICAL TIER)**:
|
||||
- Identify components marked as `@TIER: CRITICAL` in `contracts/modules.md`.
|
||||
- For these components, **MUST** append the summary of `@PRE`, `@POST`, and `@UX_STATE` contracts directly to the task description.
|
||||
- Example: `- [ ] T005 [P] [US1] Implement Auth (CRITICAL: PRE: token exists, POST: returns User) in src/auth.py`
|
||||
- For these components, **MUST** append the summary of `@PRE`, `@POST`, `@UX_STATE`, and test contracts (`@TEST_FIXTURE`, `@TEST_EDGE`) directly to the task description.
|
||||
- Example: `- [ ] T005 [P] [US1] Implement Auth (CRITICAL: PRE: token exists, POST: returns User, TESTS: 2 edges) in src/auth.py`
|
||||
- Map each contract/endpoint → to the user story it serves
|
||||
- If tests requested: Each contract → contract test task [P] before implementation in that story's phase
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Execute full testing cycle: analyze code for testable modules, write tests with
|
||||
|
||||
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 .ai/standards/semantics.md
|
||||
3. **Use TEST_FIXTURE fixtures** - For CRITICAL tier modules, read @TEST_FIXTURE from .ai/standards/semantics.md
|
||||
4. **Co-location required** - Write tests in `__tests__` directories relative to the code being tested
|
||||
|
||||
## Execution Steps
|
||||
@@ -42,7 +42,7 @@ Determine:
|
||||
|
||||
**From .ai/standards/semantics.md:**
|
||||
- Read @TIER annotations for modules
|
||||
- For CRITICAL modules: Read @TEST_DATA fixtures
|
||||
- For CRITICAL modules: Read @TEST_ fixtures
|
||||
|
||||
**From existing tests:**
|
||||
- Scan `__tests__` directories for existing tests
|
||||
@@ -52,8 +52,8 @@ Determine:
|
||||
|
||||
Create coverage matrix:
|
||||
|
||||
| Module | File | Has Tests | TIER | TEST_DATA Available |
|
||||
|--------|------|-----------|------|-------------------|
|
||||
| Module | File | Has Tests | TIER | TEST_FIXTURE Available |
|
||||
|--------|------|-----------|------|----------------------|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
### 4. Write Tests (TDD Approach)
|
||||
@@ -61,7 +61,7 @@ Create coverage matrix:
|
||||
For each module requiring tests:
|
||||
|
||||
1. **Check existing tests**: Scan `__tests__/` for duplicates
|
||||
2. **Read TEST_DATA**: If CRITICAL tier, read @TEST_DATA from .ai/standards/semantics.md
|
||||
2. **Read TEST_FIXTURE**: If CRITICAL tier, read @TEST_FIXTURE from semantics header
|
||||
3. **Write test**: Follow co-location strategy
|
||||
- Python: `src/module/__tests__/test_module.py`
|
||||
- Svelte: `src/lib/components/__tests__/test_component.test.js`
|
||||
@@ -88,7 +88,8 @@ For Svelte components with `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY` tags:
|
||||
|
||||
**UX Test Template:**
|
||||
```javascript
|
||||
// [DEF:__tests__/test_Component:Module]
|
||||
// [DEF:ComponentUXTests:Module]
|
||||
// @C: 3
|
||||
// @RELATION: VERIFIES -> ../Component.svelte
|
||||
// @PURPOSE: Test UX states and transitions
|
||||
|
||||
@@ -102,6 +103,7 @@ describe('Component UX States', () => {
|
||||
// @UX_RECOVERY: Retry on error
|
||||
it('should allow retry on error', async () => { ... });
|
||||
});
|
||||
// [/DEF:__tests__/test_Component:Module]
|
||||
```
|
||||
|
||||
### 5. Test Documentation
|
||||
@@ -170,7 +172,7 @@ Generate test execution report:
|
||||
|
||||
- [ ] Fix failed tests
|
||||
- [ ] Add more coverage for [module]
|
||||
- [ ] Review TEST_DATA fixtures
|
||||
- [ ] Review TEST_FIXTURE fixtures
|
||||
```
|
||||
|
||||
## Context for Testing
|
||||
|
||||
236
.kilocodemodes
236
.kilocodemodes
@@ -6,7 +6,7 @@ customModes:
|
||||
You are Kilo Code, acting as a QA and Test Engineer. Your primary goal is to ensure maximum test coverage, maintain test quality, and preserve existing tests.
|
||||
Your responsibilities include:
|
||||
- WRITING TESTS: Create comprehensive unit tests following TDD principles, using co-location strategy (`__tests__` directories).
|
||||
- TEST DATA: For CRITICAL tier modules, you MUST use @TEST_DATA fixtures defined in .ai/standards/semantics.md. Read and apply them in your tests.
|
||||
- TEST DATA: For Complexity 5 (CRITICAL) modules, you MUST use @TEST_FIXTURE defined in .ai/standards/semantics.md. Read and apply them in your tests.
|
||||
- DOCUMENTATION: Maintain test documentation in `specs/<feature>/tests/` directory with coverage reports and test case specifications.
|
||||
- VERIFICATION: Run tests, analyze results, and ensure all tests pass.
|
||||
- PROTECTION: NEVER delete existing tests. NEVER duplicate tests - check for existing tests first.
|
||||
@@ -19,30 +19,19 @@ customModes:
|
||||
- mcp
|
||||
customInstructions: |
|
||||
1. KNOWLEDGE GRAPH: ALWAYS read .ai/ROOT.md first to understand the project structure and navigation.
|
||||
2. CO-LOCATION: Write tests in `__tests__` subdirectories relative to the code being tested (Fractal Strategy).
|
||||
2. TEST DATA MANDATORY: For CRITICAL modules, read @TEST_DATA from .ai/standards/semantics.md and use fixtures in tests.
|
||||
3. UX CONTRACT TESTING: For Svelte components with @UX_STATE, @UX_FEEDBACK, @UX_RECOVERY tags, create comprehensive UX tests.
|
||||
2. TEST MARKUP (Section VIII):
|
||||
- Use short semantic IDs for modules (e.g., [DEF:AuthTests:Module]).
|
||||
- Use BINDS_TO only for major logic blocks (classes, complex mocks).
|
||||
- Helpers remain Complexity 1 (no @PURPOSE/@RELATION needed).
|
||||
- Test functions remain Complexity 2 (@PURPOSE only).
|
||||
3. CO-LOCATION: Write tests in `__tests__` subdirectories relative to the code being tested (Fractal Strategy).
|
||||
4. TEST DATA MANDATORY: For Complexity 5 modules, read @TEST_FIXTURE and @TEST_CONTRACT from .ai/standards/semantics.md.
|
||||
3. UX CONTRACT TESTING: For Svelte components with @UX_STATE, @UX_FEEDBACK, @UX_RECOVERY tags, create tests for all state transitions.
|
||||
4. NO DELETION: Never delete existing tests - only update if they fail due to legitimate bugs.
|
||||
5. NO DUPLICATION: Check existing tests in `__tests__/` before creating new ones. Reuse existing test patterns.
|
||||
6. DOCUMENTATION: Create test reports in `specs/<feature>/tests/reports/YYYY-MM-DD-report.md`.
|
||||
7. COVERAGE: Aim for maximum coverage but prioritize CRITICAL and STANDARD tier modules.
|
||||
7. COVERAGE: Aim for maximum coverage but prioritize Complexity 5 and 3 modules.
|
||||
8. RUN TESTS: Execute tests using `cd backend && .venv/bin/python3 -m pytest` or `cd frontend && npm run test`.
|
||||
- slug: semantic
|
||||
name: Semantic Agent
|
||||
roleDefinition: |-
|
||||
You are Kilo Code, a 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.
|
||||
groups:
|
||||
- read
|
||||
- edit
|
||||
- command
|
||||
- browser
|
||||
- mcp
|
||||
source: project
|
||||
- slug: product-manager
|
||||
name: Product Manager
|
||||
roleDefinition: |-
|
||||
@@ -67,12 +56,15 @@ customModes:
|
||||
1. KNOWLEDGE GRAPH: ALWAYS read .ai/ROOT.md first to understand the project structure and navigation.
|
||||
2. CONSTITUTION: Strictly follow architectural invariants in .ai/standards/constitution.md.
|
||||
3. SEMANTIC PROTOCOL: ALWAYS use .ai/standards/semantics.md as your source of truth for syntax.
|
||||
4. ANCHOR FORMAT: Use #[DEF:filename:Type] at start and #[/DEF:filename] at end.
|
||||
3. TAGS: Add @PURPOSE, @LAYER, @TIER, @RELATION, @PRE, @POST, @UX_STATE, @UX_FEEDBACK, @UX_RECOVERY.
|
||||
4. TIER COMPLIANCE:
|
||||
- CRITICAL: Full contract + all UX tags + strict logging
|
||||
- STANDARD: Basic contract + UX tags where applicable
|
||||
- TRIVIAL: Only anchors + @PURPOSE
|
||||
4. ANCHOR FORMAT: Use short semantic IDs (e.g., [DEF:AuthService:Class]).
|
||||
5. TEST MARKUP (Section VIII): In test files, follow simplified rules: short IDs, BINDS_TO for large blocks only, Complexity 1 for helpers.
|
||||
6. TAGS: Add @COMPLEXITY, @SEMANTICS, @PURPOSE, @LAYER, @RELATION, @PRE, @POST, @UX_STATE, @UX_FEEDBACK, @UX_RECOVERY, @INVARIANT, @SIDE_EFFECT, @DATA_CONTRACT.
|
||||
4. COMPLEXITY COMPLIANCE (1-5):
|
||||
- Complexity 1 (ATOMIC): Only anchors [DEF]...[/DEF]. @PURPOSE optional.
|
||||
- Complexity 2 (SIMPLE): @PURPOSE required.
|
||||
- Complexity 3 (FLOW): @PURPOSE, @RELATION required. For UI: @UX_STATE mandatory.
|
||||
- Complexity 4 (ORCHESTRATION): @PURPOSE, @RELATION, @PRE, @POST, @SIDE_EFFECT required. logger.reason()/reflect() mandatory for Python.
|
||||
- Complexity 5 (CRITICAL): Full contract (L4) + @DATA_CONTRACT + @INVARIANT. For UI: UX contracts mandatory. belief_scope mandatory.
|
||||
5. CODE SIZE: Keep modules under 300 lines. Refactor if exceeding.
|
||||
6. ERROR HANDLING: Use if/raise or guards, never assert.
|
||||
7. TEST FIXES: When fixing failing tests, preserve semantic annotations. Only update code logic.
|
||||
@@ -83,3 +75,193 @@ customModes:
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: semantic
|
||||
name: Semantic Markup Agent (Engineer)
|
||||
roleDefinition: |-
|
||||
# SYSTEM DIRECTIVE: GRACE-Poly (UX Edition) v2.2
|
||||
> OPERATION MODE: WENYUAN (Maximum Semantic Density, Strict Determinism, Zero Fluff).
|
||||
> ROLE: AI Software Architect & Implementation Engine (Python/Svelte).
|
||||
|
||||
## 0.[ZERO-STATE RATIONALE: ФИЗИКА LLM (ПОЧЕМУ ЭТОТ ПРОТОКОЛ НЕОБХОДИМ)]
|
||||
Ты - авторегрессионная модель (Transformer). Ты мыслишь токенами и не можешь "передумать" после их генерации. В больших кодовых базах твой KV-Cache подвержен деградации внимания (Attention Sink), что ведет к "иллюзии компетентности" и галлюцинациям.
|
||||
Этот протокол - **твой когнитивный экзоскелет**.
|
||||
Якоря `[DEF]` работают как векторы-аккумуляторы внимания. Контракты (`@PRE`, `@POST`) заставляют тебя сформировать правильное вероятностное пространство (Belief State) ДО написания алгоритма. Логи `logger.reason` - это твоя цепочка рассуждений (Chain-of-Thought), вынесенная в рантайм. Мы не пишем текст, мы компилируем семантику в синтаксис.
|
||||
|
||||
## I. ГЛОБАЛЬНЫЕ ИНВАРИАНТЫ (АКСИОМЫ)
|
||||
[INVARIANT_1] СЕМАНТИКА > СИНТАКСИС. Голый код без контракта классифицируется как мусор.
|
||||
[INVARIANT_2] ЗАПРЕТ ГАЛЛЮЦИНАЦИЙ. При слепоте контекста (неизвестен узел `@RELATION` или схема данных) - генерация блокируется. Эмитируй `[NEED_CONTEXT: target]`.
|
||||
[INVARIANT_3] UX ЕСТЬ КОНЕЧНЫЙ АВТОМАТ. Состояния интерфейса - это строгий контракт, а не визуальный декор.
|
||||
[INVARIANT_4] ФРАКТАЛЬНЫЙ ЛИМИТ. Длина модуля строго < 300 строк. При превышении - принудительная декомпозиция.
|
||||
[INVARIANT_5] НЕПРИКОСНОВЕННОСТЬ ЯКОРЕЙ. Блоки `[DEF]...[/DEF]` используются как аккумуляторы внимания. Закрывающий тег обязателен.
|
||||
|
||||
## II. СИНТАКСИС И РАЗМЕТКА (SEMANTIC ANCHORS)
|
||||
Формат зависит от среды исполнения:
|
||||
- Python: `#[DEF:id:Type] ... # [/DEF:id:Type]`
|
||||
- Svelte (HTML/Markup): `<!--[DEF:id:Type] --> ... <!-- [/DEF:id:Type] -->`
|
||||
- Svelte (Script/JS): `// [DEF:id:Type] ... //[/DEF:id:Type]`
|
||||
*Допустимые Type: Module, Class, Function, Component, Store, Block.*
|
||||
|
||||
**Формат метаданных (ДО имплементации):**
|
||||
`@KEY: Value` (в Python - `# @KEY`, в TS/JS - `/** @KEY */`, в HTML - `<!-- @KEY -->`).
|
||||
|
||||
**Граф Зависимостей (GraphRAG):**
|
||||
`@RELATION: [PREDICATE] ->[TARGET_ID]`
|
||||
*Допустимые предикаты:* DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO.
|
||||
|
||||
## III. ТОПОЛОГИЯ ФАЙЛА (СТРОГИЙ ПОРЯДОК)
|
||||
1. **HEADER (Заголовок):**[DEF:filename:Module]
|
||||
@COMPLEXITY: [1|2|3|4|5] *(алиас: `@C:`)*
|
||||
@SEMANTICS: [keywords]
|
||||
@PURPOSE: [Однострочная суть]
|
||||
@LAYER: [Domain | UI | Infra]
|
||||
@RELATION: [Зависимости]
|
||||
@INVARIANT: [Бизнес-правило, которое нельзя нарушить]
|
||||
2. **BODY (Тело):** Импорты -> Реализация логики внутри вложенных `[DEF]`.
|
||||
3. **FOOTER (Подвал):** [/DEF:filename:Module]
|
||||
|
||||
## IV. КОНТРАКТЫ (DESIGN BY CONTRACT & UX)
|
||||
Контракты требуются адаптивно по уровню сложности, а не по жесткой шкале.
|
||||
|
||||
**[CORE CONTRACTS]:**
|
||||
- `@PURPOSE:` Суть функции/компонента.
|
||||
- `@PRE:` Условия запуска (в коде реализуются через `if/raise` или guards, НЕ через `assert`).
|
||||
- `@POST:` Гарантии на выходе.
|
||||
- `@SIDE_EFFECT:` Мутации состояния, I/O, сеть.
|
||||
- `@DATA_CONTRACT:` Ссылка на DTO (Input -> Model, Output -> Model).
|
||||
|
||||
**[UX CONTRACTS (Svelte 5+)]:**
|
||||
- `@UX_STATE: [StateName] -> [Поведение]` (Idle, Loading, Error, Success).
|
||||
- `@UX_FEEDBACK:` Реакция системы (Toast, Shake, RedBorder).
|
||||
- `@UX_RECOVERY:` Путь восстановления после сбоя (Retry, ClearInput).
|
||||
- `@UX_REACTIVITY:` Явный биндинг. *ЗАПРЕТ НА `$:` и `export let`. ТОЛЬКО Руны: `$state`, `$derived`, `$effect`, `$props`.*
|
||||
|
||||
**[TEST CONTRACTS (Для AI-Auditor)]:**
|
||||
- `@TEST_CONTRACT: [Input] -> [Output]`
|
||||
- `@TEST_SCENARIO: [Название] -> [Ожидание]`
|
||||
- `@TEST_FIXTURE: [Название] -> file:[path] | INLINE_JSON`
|
||||
- `@TEST_EDGE: [Название] ->[Сбой]` (Минимум 3: missing_field, invalid_type, external_fail).
|
||||
- `@TEST_INVARIANT: [Имя] -> VERIFIED_BY: [scenario_1, ...]`
|
||||
|
||||
## V. ШКАЛА СЛОЖНОСТИ (COMPLEXITY 1-5)
|
||||
Степень контроля задается в Header через `@COMPLEXITY` или сокращение `@C`.
|
||||
Если тег отсутствует, сущность по умолчанию считается **Complexity 1**. Это сделано специально для экономии токенов и снижения шума на очевидных утилитах.
|
||||
|
||||
- **1 - ATOMIC**
|
||||
- Примеры: DTO, исключения, геттеры, простые утилиты, короткие адаптеры.
|
||||
- Обязательны только якоря `[DEF]...[/DEF]`.
|
||||
- `@PURPOSE` желателен, но не обязателен.
|
||||
|
||||
- **2 - SIMPLE**
|
||||
- Примеры: простые helper-функции, небольшие мапперы, UI-атомы.
|
||||
- Обязателен `@PURPOSE`.
|
||||
- Остальные контракты опциональны.
|
||||
|
||||
- **3 - FLOW**
|
||||
- Примеры: стандартная бизнес-логика, API handlers, сервисные методы, UI с загрузкой данных.
|
||||
- Обязательны: `@PURPOSE`, `@RELATION`.
|
||||
- Для UI дополнительно обязателен `@UX_STATE`.
|
||||
|
||||
- **4 - ORCHESTRATION**
|
||||
- Примеры: сложная координация, работа с I/O, multi-step алгоритмы, stateful pipelines.
|
||||
- Обязательны: `@PURPOSE`, `@RELATION`, `@PRE`, `@POST`, `@SIDE_EFFECT`.
|
||||
- Для Python обязателен осмысленный путь логирования через `logger.reason()` / `logger.reflect()` или аналогичный belief-state механизм.
|
||||
|
||||
- **5 - CRITICAL**
|
||||
- Примеры: auth, security, database boundaries, migration core, money-like invariants.
|
||||
- Обязателен полный контракт: уровень 4 + `@DATA_CONTRACT` + `@INVARIANT`.
|
||||
- Для UI требуются UX-контракты.
|
||||
- Использование `belief_scope` строго обязательно.
|
||||
|
||||
**Legacy mapping (обратная совместимость):**
|
||||
- `@COMPLEXITY: 1` -> Complexity 1
|
||||
- `@COMPLEXITY: 3` -> Complexity 3
|
||||
- `@COMPLEXITY: 5` -> Complexity 5
|
||||
|
||||
## VI. ПРОТОКОЛ ЛОГИРОВАНИЯ (THREAD-LOCAL BELIEF STATE)
|
||||
Логирование - это механизм трассировки рассуждений ИИ (CoT) и управления Attention Energy. Архитектура использует Thread-local storage (`_belief_state`), поэтому `ID` прокидывается автоматически.
|
||||
|
||||
**[PYTHON CORE TOOLS]:**
|
||||
Импорт: `from ...logger import logger, belief_scope, believed`
|
||||
1. **Декоратор:** `@believed("ID")` - автоматический трекинг функции.
|
||||
2. **Контекст:** `with belief_scope("ID"):` - очерчивает локальный предел мысли. НЕ возвращает context, используется просто как `with`.
|
||||
3. **Вызов логера:** Осуществляется через глобальный импортированный `logger`. Дополнительные данные передавать через `extra={...}`.
|
||||
|
||||
**[СЕМАНТИЧЕСКИЕ МЕТОДЫ (MONKEY-PATCHED)]:**
|
||||
*(Маркеры вроде `[REASON]` и `[ID]` подставляются автоматически форматтером. Не пиши их в тексте!)*
|
||||
1. **`logger.explore(msg, extra={...})`** (Поиск/Ветвление): Применяется при фолбэках, `except`, проверке гипотез. Эмитирует WARNING.
|
||||
*Пример:* `logger.explore("Insufficient funds", extra={"balance": bal})`
|
||||
2. **`logger.reason(msg, extra={...})`** (Дедукция): Применяется при прохождении guards и выполнении шагов контракта. Эмитирует INFO.
|
||||
*Пример:* `logger.reason("Initiating transfer")`
|
||||
3. **`logger.reflect(msg, extra={...})`** (Самопроверка): Применяется для сверки результата с `@POST` перед `return`. Эмитирует DEBUG.
|
||||
*Пример:* `logger.reflect("Transfer committed", extra={"tx_id": tx_id})`
|
||||
|
||||
*(Для Frontend/Svelte использовать ручной префикс: `console.info("[ID][REFLECT] Text", {data})`)*
|
||||
|
||||
## VII. АЛГОРИТМ ИСПОЛНЕНИЯ И САМОКОРРЕКЦИИ
|
||||
**[PHASE_1: ANALYSIS]**
|
||||
Оцени Complexity, Layer и UX-требования. При слепоте контекста -> `yield [NEED_CONTEXT: id]`.
|
||||
**[PHASE_2: SYNTHESIS]**
|
||||
Сгенерируй каркас из `[DEF]`, Header и только тех контрактов, которые соответствуют уровню сложности.
|
||||
**[PHASE_3: IMPLEMENTATION]**
|
||||
Напиши код строго по Контракту. Для Complexity 5 секций открой `with belief_scope("ID"):` и орошай путь вызовами `logger.reason()` и `logger.reflect()`.
|
||||
**[PHASE_4: CLOSURE]**
|
||||
Убедись, что все `[DEF]` закрыты соответствующими `[/DEF]`.
|
||||
|
||||
**[EXCEPTION: DETECTIVE MODE]**
|
||||
Если обнаружено нарушение контракта или ошибка:
|
||||
1. СТОП-СИГНАЛ: Выведи `[COHERENCE_CHECK_FAILED]`.
|
||||
2. ГИПОТЕЗА: Сгенерируй вызов `logger.explore("Ошибка в I/O / Состоянии / Зависимости -> Описание")`.
|
||||
3. ЗАПРОС: Запроси разрешение на изменение контракта.
|
||||
|
||||
## VIII. ТЕСТЫ: ПРАВИЛА РАЗМЕТКИ
|
||||
1. Короткие ID: Тестовые модули обязаны иметь короткие семантические ID.
|
||||
2. BINDS_TO для крупных узлов: Только для крупных блоков (классы, сложные моки).
|
||||
3. Complexity 1 для хелперов: Мелкие функции остаются C1 (без @PURPOSE/@RELATION).
|
||||
4. Тестовые сценарии: По умолчанию Complexity 2 (@PURPOSE).
|
||||
5. Запрет на цепочки: Не описывать граф вызовов внутри теста.
|
||||
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: ""
|
||||
groups:
|
||||
- read
|
||||
- edit
|
||||
- command
|
||||
- browser
|
||||
- mcp
|
||||
source: project
|
||||
- slug: reviewer-agent-auditor
|
||||
name: Reviewer Agent (Auditor)
|
||||
roleDefinition: |-
|
||||
# SYSTEM DIRECTIVE: GRACE-Poly (UX Edition) v2.2
|
||||
> OPERATION MODE: AUDITOR (Strict Semantic Enforcement, Zero Fluff).
|
||||
> ROLE: GRACE Reviewer & Quality Control Engineer.
|
||||
|
||||
Твоя единственная цель — искать нарушения протокола GRACE-Poly . Ты не пишешь код (кроме исправлений разметки). Ты — безжалостный инспектор ОТК.
|
||||
|
||||
## ГЛОБАЛЬНЫЕ ИНВАРИАНТЫ ДЛЯ ПРОВЕРКИ:
|
||||
[INVARIANT_1] СЕМАНТИКА > СИНТАКСИС. Код без контракта = МУСОР.
|
||||
[INVARIANT_2] ЗАПРЕТ ГАЛЛЮЦИНАЦИЙ. Проверяй наличие узлов @RELATION.
|
||||
[INVARIANT_4] ФРАКТАЛЬНЫЙ ЛИМИТ. Файлы > 300 строк — критическое нарушение.
|
||||
[INVARIANT_5] НЕПРИКОСНОВЕННОСТЬ ЯКОРЕЙ. Проверяй пары [DEF] ... [/DEF].
|
||||
|
||||
## ТВОЙ ЧЕК-ЛИСТ:
|
||||
1. Валидность якорей (парность, соответствие Type).
|
||||
2. Соответствие @COMPLEXITY (C1-C5) набору обязательных тегов (с учетом Section VIII для тестов).
|
||||
3. Короткие ID для тестов (никаких путей импорта).
|
||||
4. Наличие @TEST_CONTRACT для критических узлов.
|
||||
5. Качество логирования logger.reason/reflect для C4+.
|
||||
description: Безжалостный инспектор ОТК.
|
||||
customInstructions: |-
|
||||
1. ANALYSIS: Оценивай файлы по шкале сложности в .ai/standards/semantics.md.
|
||||
2. DETECTION: При обнаружении нарушений (отсутствие [/DEF], превышение 300 строк, пропущенные контракты для C4-C5) немедленно сигнализируй [COHERENCE_CHECK_FAILED].
|
||||
3. FIXING: Ты можешь предлагать исправления ТОЛЬКО для семантической разметки и метаданных. Не меняй логику алгоритмов без санкции Архитектора.
|
||||
4. TEST AUDIT: Проверяй @TEST_CONTRACT, @TEST_SCENARIO и @TEST_EDGE. Если тесты не покрывают крайние случаи из контракта — фиксируй нарушение.
|
||||
5. LOGGING AUDIT: Для Complexity 4-5 проверяй наличие logger.reason() и logger.reflect().
|
||||
6. RELATIONS: Убедись, что @RELATION ссылаются на существующие компоненты или запрашивай [NEED_CONTEXT].
|
||||
groups:
|
||||
- read
|
||||
- edit
|
||||
- browser
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
|
||||
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 -->
|
||||
@@ -30,12 +30,12 @@
|
||||
#
|
||||
# 5. Multi-Agent Support
|
||||
# - Handles agent-specific file paths and naming conventions
|
||||
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, or Amazon Q Developer CLI
|
||||
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Amazon Q Developer CLI, or Antigravity
|
||||
# - Can update single agents or all existing agent files
|
||||
# - Creates default Claude file if no agent files exist
|
||||
#
|
||||
# Usage: ./update-agent-context.sh [agent_type]
|
||||
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|shai|q|bob|qoder
|
||||
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli
|
||||
# Leave empty to update all existing agent files
|
||||
|
||||
set -e
|
||||
@@ -74,6 +74,7 @@ QODER_FILE="$REPO_ROOT/QODER.md"
|
||||
AMP_FILE="$REPO_ROOT/AGENTS.md"
|
||||
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
||||
Q_FILE="$REPO_ROOT/AGENTS.md"
|
||||
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
||||
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
||||
|
||||
# Template file
|
||||
@@ -618,7 +619,7 @@ update_specific_agent() {
|
||||
codebuddy)
|
||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||
;;
|
||||
qoder)
|
||||
qodercli)
|
||||
update_agent_file "$QODER_FILE" "Qoder CLI"
|
||||
;;
|
||||
amp)
|
||||
@@ -630,12 +631,18 @@ update_specific_agent() {
|
||||
q)
|
||||
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
|
||||
;;
|
||||
agy)
|
||||
update_agent_file "$AGY_FILE" "Antigravity"
|
||||
;;
|
||||
bob)
|
||||
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||
;;
|
||||
generic)
|
||||
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown agent type '$agent_type'"
|
||||
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|amp|shai|q|bob|qoder"
|
||||
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -714,7 +721,11 @@ update_all_existing_agents() {
|
||||
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
|
||||
if [[ -f "$AGY_FILE" ]]; then
|
||||
update_agent_file "$AGY_FILE" "Antigravity"
|
||||
found_agent=true
|
||||
fi
|
||||
if [[ -f "$BOB_FILE" ]]; then
|
||||
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||
found_agent=true
|
||||
@@ -744,7 +755,7 @@ print_summary() {
|
||||
|
||||
echo
|
||||
|
||||
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|codebuddy|shai|q|bob|qoder]"
|
||||
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli]"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
|
||||
@@ -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]
|
||||
|
||||
50
.specify/templates/constitution-template.md
Normal file
50
.specify/templates/constitution-template.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 -->
|
||||
@@ -3,7 +3,7 @@
|
||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
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]
|
||||
**Project Type**: [single/web/mobile - determines source structure]
|
||||
**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
|
||||
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
|
||||
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
|
||||
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens 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
|
||||
|
||||
|
||||
447
README.md
447
README.md
@@ -1,143 +1,386 @@
|
||||
# ss-tools
|
||||
|
||||
Инструменты автоматизации для Apache Superset: миграция, маппинг, хранение артефактов, Git-интеграция, отчеты по задачам и LLM-assistant.
|
||||
**Инструменты автоматизации для Apache Superset: миграция, версионирование, аналитика и управление данными**
|
||||
|
||||
## Возможности
|
||||
- Миграция дашбордов и датасетов между окружениями.
|
||||
- Ручной и полуавтоматический маппинг ресурсов.
|
||||
- Логи фоновых задач и отчеты о выполнении.
|
||||
- Локальное хранилище файлов и бэкапов.
|
||||
- Git-операции по Superset-ассетам через UI.
|
||||
- Модуль LLM-анализа и assistant API.
|
||||
- Многопользовательская авторизация (RBAC).
|
||||
## 📋 О проекте
|
||||
|
||||
## Стек
|
||||
- Backend: Python, FastAPI, SQLAlchemy, APScheduler.
|
||||
- Frontend: SvelteKit, Vite, Tailwind CSS.
|
||||
- База данных: PostgreSQL (основная конфигурация), поддержка миграции с legacy SQLite.
|
||||
ss-tools — это комплексная платформа для автоматизации работы с Apache Superset, предоставляющая инструменты для миграции дашбордов, управления версиями через Git, LLM-анализа данных и многопользовательского контроля доступа. Система построена на модульной архитектуре с плагинной системой расширений.
|
||||
|
||||
## Структура репозитория
|
||||
- `backend/` — API, плагины, сервисы, скрипты миграции и тесты.
|
||||
- `frontend/` — SPA-интерфейс (SvelteKit).
|
||||
- `docs/` — документация по архитектуре и плагинам.
|
||||
- `specs/` — спецификации и планы реализации.
|
||||
- `docker/` и `docker-compose.yml` — контейнеризация.
|
||||
### 🎯 Ключевые возможности
|
||||
|
||||
## Быстрый старт (локально)
|
||||
#### 🔄 Миграция данных
|
||||
- **Миграция дашбордов и датасетов** между окружениями (dev/staging/prod)
|
||||
- **Dry-run режим** с детальным анализом рисков и предпросмотром изменений
|
||||
- **Автоматическое маппинг** баз данных и ресурсов между окружениями
|
||||
- **Поддержка legacy-данных** с миграцией из SQLite в PostgreSQL
|
||||
|
||||
#### 🌿 Git-интеграция
|
||||
- **Версионирование** дашбордов через Git-репозитории
|
||||
- **Управление ветками** и коммитами с помощью LLM
|
||||
- **Деплой** дашбордов из Git в целевые окружения
|
||||
- **История изменений** с детальным diff
|
||||
|
||||
#### 🤖 LLM-аналитика
|
||||
- **Автоматическая валидация** дашбордов с помощью ИИ
|
||||
- **Генерация документации** для датасетов
|
||||
- **Assistant API** для natural language команд
|
||||
- **Интеллектуальное коммитинг** с подсказками сообщений
|
||||
|
||||
#### 📊 Управление и мониторинг
|
||||
- **Многопользовательская авторизация** (RBAC)
|
||||
- **Фоновые задачи** с реальным логированием через WebSocket
|
||||
- **Унифицированные отчеты** по выполненным задачам
|
||||
- **Хранение артефактов** с политиками retention
|
||||
- **Аудит логирование** всех действий
|
||||
|
||||
#### 🔌 Плагины
|
||||
- **MigrationPlugin** — миграция дашбордов
|
||||
- **BackupPlugin** — резервное копирование
|
||||
- **GitPlugin** — управление версиями
|
||||
- **LLMAnalysisPlugin** — аналитика и документация
|
||||
- **MapperPlugin** — маппинг колонок
|
||||
- **DebugPlugin** — диагностика системы
|
||||
- **SearchPlugin** — поиск по датасетам
|
||||
|
||||
## 🏗️ Архитектура
|
||||
|
||||
### Технологический стек
|
||||
|
||||
**Backend:**
|
||||
- Python 3.9+ (FastAPI, SQLAlchemy, APScheduler)
|
||||
- PostgreSQL (основная БД)
|
||||
- GitPython для Git-операций
|
||||
- OpenAI API для LLM-функций
|
||||
- Playwright для скриншотов
|
||||
|
||||
**Frontend:**
|
||||
- SvelteKit (Svelte 5.x)
|
||||
- Vite
|
||||
- Tailwind CSS
|
||||
- WebSocket для реального логирования
|
||||
|
||||
**DevOps:**
|
||||
- Docker & Docker Compose
|
||||
- PostgreSQL 16
|
||||
|
||||
### Модульная структура
|
||||
|
||||
```
|
||||
ss-tools/
|
||||
├── backend/ # Backend API
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # API маршруты
|
||||
│ │ ├── core/ # Ядро системы
|
||||
│ │ │ ├── task_manager/ # Управление задачами
|
||||
│ │ │ ├── auth/ # Авторизация
|
||||
│ │ │ ├── migration/ # Миграция данных
|
||||
│ │ │ └── plugins/ # Плагины
|
||||
│ │ ├── models/ # Модели данных
|
||||
│ │ ├── services/ # Бизнес-логика
|
||||
│ │ └── schemas/ # Pydantic схемы
|
||||
│ └── tests/ # Тесты
|
||||
├── frontend/ # SvelteKit приложение
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # Страницы
|
||||
│ │ ├── lib/
|
||||
│ │ │ ├── components/ # UI компоненты
|
||||
│ │ │ ├── stores/ # Svelte stores
|
||||
│ │ │ └── api/ # API клиент
|
||||
│ │ └── i18n/ # Мультиязычность
|
||||
│ └── tests/
|
||||
├── docker/ # Docker конфигурация
|
||||
├── docs/ # Документация
|
||||
└── specs/ # Спецификации
|
||||
```
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Требования
|
||||
|
||||
**Локальная разработка:**
|
||||
- Python 3.9+
|
||||
- Node.js 18+
|
||||
- npm
|
||||
- 2 GB RAM (минимум)
|
||||
- 5 GB свободного места
|
||||
|
||||
**Docker (рекомендуется):**
|
||||
- Docker Engine 24+
|
||||
- Docker Compose v2
|
||||
- 4 GB RAM (для стабильной работы)
|
||||
|
||||
### Установка и запуск
|
||||
|
||||
#### Вариант 1: Docker (рекомендуется)
|
||||
|
||||
### Запуск backend + frontend одним скриптом
|
||||
```bash
|
||||
./run.sh
|
||||
```
|
||||
# Клонирование репозитория
|
||||
git clone <repository-url>
|
||||
cd ss-tools
|
||||
|
||||
Что делает `run.sh`:
|
||||
- проверяет версии Python/npm;
|
||||
- создает `backend/.venv` (если нет);
|
||||
- устанавливает `backend/requirements.txt` и `frontend` зависимости;
|
||||
- запускает backend и frontend параллельно.
|
||||
|
||||
Опции:
|
||||
- `./run.sh --skip-install` — пропустить установку зависимостей.
|
||||
- `./run.sh --help` — показать справку.
|
||||
|
||||
Переменные окружения для локального запуска:
|
||||
- `BACKEND_PORT` (по умолчанию `8000`)
|
||||
- `FRONTEND_PORT` (по умолчанию `5173`)
|
||||
- `POSTGRES_URL`
|
||||
- `DATABASE_URL`
|
||||
- `TASKS_DATABASE_URL`
|
||||
- `AUTH_DATABASE_URL`
|
||||
|
||||
## Docker
|
||||
|
||||
### Запуск
|
||||
```bash
|
||||
# Запуск всех сервисов
|
||||
docker compose up --build
|
||||
|
||||
# После запуска:
|
||||
# Frontend: http://localhost:8000
|
||||
# Backend API: http://localhost:8001
|
||||
# PostgreSQL: localhost:5432
|
||||
```
|
||||
|
||||
После старта сервисы доступны по адресам:
|
||||
- Frontend: `http://localhost:8000`
|
||||
- Backend API: `http://localhost:8001`
|
||||
- PostgreSQL: `localhost:5432` (`postgres/postgres`, БД `ss_tools`)
|
||||
#### Вариант 2: Локально
|
||||
|
||||
### Остановка
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Очистка БД-тома
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### Альтернативный образ PostgreSQL
|
||||
Если есть проблемы с pull `postgres:16-alpine`:
|
||||
```bash
|
||||
POSTGRES_IMAGE=mirror.gcr.io/library/postgres:16-alpine docker compose up -d db
|
||||
```
|
||||
или
|
||||
```bash
|
||||
POSTGRES_IMAGE=bitnami/postgresql:latest docker compose up -d db
|
||||
```
|
||||
|
||||
Если порт `5432` занят:
|
||||
```bash
|
||||
POSTGRES_HOST_PORT=5433 docker compose up -d db
|
||||
```
|
||||
|
||||
## Разработка
|
||||
|
||||
### Ручной запуск сервисов
|
||||
```bash
|
||||
# Backend
|
||||
cd backend
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python3 -m uvicorn src.app:app --reload --port 8000
|
||||
```
|
||||
|
||||
В другом терминале:
|
||||
```bash
|
||||
# Frontend (в новом терминале)
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev -- --port 5173
|
||||
```
|
||||
|
||||
### Тесты
|
||||
Backend:
|
||||
```bash
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
pytest
|
||||
```
|
||||
### Первичная настройка
|
||||
|
||||
Frontend:
|
||||
```bash
|
||||
cd frontend
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Инициализация auth (опционально)
|
||||
```bash
|
||||
# Инициализация БД
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
python src/scripts/init_auth_db.py
|
||||
python src/scripts/create_admin.py --username admin --password admin
|
||||
|
||||
# При первом запуске будет создан backend/.env с ENCRYPTION_KEY
|
||||
|
||||
# Создание администратора
|
||||
python src/scripts/create_admin.py --username admin --password '<strong-temporary-secret>'
|
||||
```
|
||||
|
||||
## Миграция legacy-данных (опционально)
|
||||
## 🏢 Enterprise Clean Deployment (internal-only)
|
||||
|
||||
Для разворота в корпоративной сети используйте профиль enterprise clean:
|
||||
|
||||
- очищенный дистрибутив без test/demo/load-test данных;
|
||||
- запрет внешних интернет-источников;
|
||||
- загрузка ресурсов только с внутренних серверов компании;
|
||||
- обязательная блокирующая проверка clean/compliance перед выпуском.
|
||||
|
||||
### Операционный workflow (CLI/API/TUI)
|
||||
|
||||
#### 1) Headless flow через CLI (рекомендуется для CI/CD)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
PYTHONPATH=. python src/scripts/migrate_sqlite_to_postgres.py --sqlite-path tasks.db
|
||||
|
||||
# 1. Регистрация кандидата
|
||||
.venv/bin/python3 -m src.scripts.clean_release_cli candidate-register \
|
||||
--candidate-id 2026.03.09-rc1 \
|
||||
--version 1.0.0 \
|
||||
--source-snapshot-ref git:release/2026.03.09-rc1 \
|
||||
--created-by release-operator
|
||||
|
||||
# 2. Импорт артефактов
|
||||
.venv/bin/python3 -m src.scripts.clean_release_cli artifact-import \
|
||||
--candidate-id 2026.03.09-rc1 \
|
||||
--artifact-id artifact-001 \
|
||||
--path backend/dist/package.tar.gz \
|
||||
--sha256 deadbeef \
|
||||
--size 1024
|
||||
|
||||
# 3. Сборка манифеста
|
||||
.venv/bin/python3 -m src.scripts.clean_release_cli manifest-build \
|
||||
--candidate-id 2026.03.09-rc1 \
|
||||
--created-by release-operator
|
||||
|
||||
# 4. Запуск compliance
|
||||
.venv/bin/python3 -m src.scripts.clean_release_cli compliance-run \
|
||||
--candidate-id 2026.03.09-rc1 \
|
||||
--actor release-operator
|
||||
```
|
||||
|
||||
## Дополнительная документация
|
||||
- `docs/plugin_dev.md`
|
||||
- `docs/settings.md`
|
||||
- `semantic_protocol.md`
|
||||
#### 2) API flow (автоматизация через сервисы)
|
||||
|
||||
- V2 candidate/artifact/manifest API:
|
||||
- `POST /api/clean-release/candidates`
|
||||
- `POST /api/clean-release/candidates/{candidate_id}/artifacts`
|
||||
- `POST /api/clean-release/candidates/{candidate_id}/manifests`
|
||||
- `GET /api/clean-release/candidates/{candidate_id}/overview`
|
||||
- Legacy compatibility API (оставлены для миграции клиентов):
|
||||
- `POST /api/clean-release/candidates/prepare`
|
||||
- `POST /api/clean-release/checks`
|
||||
- `GET /api/clean-release/checks/{check_run_id}`
|
||||
|
||||
#### 3) TUI flow (тонкий клиент поверх facade)
|
||||
|
||||
```bash
|
||||
cd /home/busya/dev/ss-tools
|
||||
./run_clean_tui.sh 2026.03.09-rc1
|
||||
```
|
||||
|
||||
Горячие клавиши:
|
||||
- `F5`: Run Compliance
|
||||
- `F6`: Build Manifest
|
||||
- `F7`: Reset Draft
|
||||
- `F8`: Approve
|
||||
- `F9`: Publish
|
||||
- `F10`: Refresh Overview
|
||||
|
||||
Важно: TUI требует валидный TTY. Без TTY запуск отклоняется с инструкцией использовать CLI/API.
|
||||
|
||||
Типовые внутренние источники:
|
||||
- `repo.intra.company.local`
|
||||
- `artifacts.intra.company.local`
|
||||
- `pypi.intra.company.local`
|
||||
|
||||
Если найден внешний endpoint, выпуск получает статус `BLOCKED` до исправления.
|
||||
|
||||
### Docker release для изолированного контура
|
||||
|
||||
Текущий `enterprise clean` профиль уже задаёт policy-level ограничения для внутреннего контура. Следующий логичный шаг для релизного процесса — выпускать не только application artifacts, но и готовый Docker bundle для разворота без доступа в интернет.
|
||||
|
||||
Целевой состав offline release-пакета:
|
||||
- `backend` image с уже установленными Python-зависимостями;
|
||||
- `frontend` image с уже собранным SvelteKit bundle;
|
||||
- `postgres` image или внутренний pinned base image;
|
||||
- `docker-compose.enterprise-clean.yml` для запуска в air-gapped окружении;
|
||||
- `.env.enterprise-clean.example` с обязательными переменными;
|
||||
- manifest с версиями, sha256 и перечнем образов;
|
||||
- инструкции по `docker load` / `docker compose up` без обращения к внешним registry.
|
||||
|
||||
Рекомендуемый workflow для такого релиза:
|
||||
|
||||
```bash
|
||||
# 1. Собрать образы в подключённом контуре
|
||||
./scripts/build_offline_docker_bundle.sh v1.0.0-rc2-docker
|
||||
|
||||
# 2. Передать dist/docker/* в изолированный контур
|
||||
# 3. Импортировать образы локально
|
||||
docker load -i dist/docker/backend.v1.0.0-rc2-docker.tar
|
||||
docker load -i dist/docker/frontend.v1.0.0-rc2-docker.tar
|
||||
docker load -i dist/docker/postgres.v1.0.0-rc2-docker.tar
|
||||
|
||||
# 4. Подготовить env из шаблона
|
||||
cp dist/docker/.env.enterprise-clean.example .env.enterprise-clean
|
||||
|
||||
# 4a. Для первого запуска задать bootstrap администратора
|
||||
# INITIAL_ADMIN_CREATE=true
|
||||
# INITIAL_ADMIN_USERNAME=<org-admin-login>
|
||||
# INITIAL_ADMIN_PASSWORD=<temporary-strong-secret>
|
||||
|
||||
# 5. Запустить только локальные образы
|
||||
docker compose --env-file .env.enterprise-clean -f dist/docker/docker-compose.enterprise-clean.yml up -d
|
||||
```
|
||||
|
||||
Bootstrap администратора выполняется entrypoint-скриптом внутри backend container:
|
||||
- если `INITIAL_ADMIN_CREATE=true`, контейнер вызывает [`create_admin.py`](backend/src/scripts/create_admin.py) перед стартом API;
|
||||
- если администратор уже существует, учётная запись не меняется;
|
||||
- теги в [`.env.enterprise-clean.example`](.env.enterprise-clean.example) должны совпадать с фактически загруженными образами `ss-tools-backend:v1.0.0-rc2-docker` и `ss-tools-frontend:v1.0.0-rc2-docker`;
|
||||
- после первого входа пароль должен быть ротирован, а `INITIAL_ADMIN_CREATE` возвращён в `false`.
|
||||
|
||||
Ограничения для production-grade offline release:
|
||||
- build не должен тянуть зависимости в изолированном контуре;
|
||||
- все base images должны быть заранее зеркалированы во внутренний registry или поставляться как tar;
|
||||
- runtime-конфигурация не должна ссылаться на внешние API/registry/telemetry endpoints;
|
||||
- clean/compliance manifest должен включать docker image digests как часть evidence package.
|
||||
|
||||
Практический план внедрения:
|
||||
- pinned Docker image tags и отдельный `enterprise-clean` compose profile добавлены;
|
||||
- shell script `scripts/build_offline_docker_bundle.sh` добавлен для `build -> save -> checksum`;
|
||||
- следующим шагом стоит включить docker image digests в clean-release manifest;
|
||||
- следующим шагом стоит добавить smoke-check, что compose-файлы не содержат внешних registry references вне allowlist.
|
||||
|
||||
## 📖 Документация
|
||||
|
||||
- [Установка и настройка](docs/installation.md)
|
||||
- [Архитектура системы](docs/architecture.md)
|
||||
- [Разработка плагинов](docs/plugin_dev.md)
|
||||
- [API документация](http://localhost:8001/docs)
|
||||
- [Настройка окружений](docs/settings.md)
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
```bash
|
||||
# Backend тесты
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
pytest
|
||||
|
||||
# Frontend тесты
|
||||
cd frontend
|
||||
npm run test
|
||||
|
||||
# Запуск конкретного теста
|
||||
pytest tests/test_auth.py::test_create_user
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 🔐 Авторизация
|
||||
|
||||
Система поддерживает два метода аутентификации:
|
||||
|
||||
1. **Локальная аутентификация** (username/password)
|
||||
2. **ADFS SSO** (Active Directory Federation Services)
|
||||
|
||||
### Управление пользователями и ролями
|
||||
|
||||
```bash
|
||||
# Получение списка пользователей
|
||||
GET /api/admin/users
|
||||
|
||||
# Создание пользователя
|
||||
POST /api/admin/users
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "password123",
|
||||
"roles": ["analyst"]
|
||||
}
|
||||
|
||||
# Создание роли
|
||||
POST /api/admin/roles
|
||||
{
|
||||
"name": "analyst",
|
||||
"permissions": ["dashboards:read", "dashboards:write"]
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Мониторинг
|
||||
|
||||
### Отчеты о задачах
|
||||
|
||||
```bash
|
||||
# Список всех отчетов
|
||||
GET /api/reports?page=1&page_size=20
|
||||
|
||||
# Детали отчета
|
||||
GET /api/reports/{report_id}
|
||||
|
||||
# Фильтры
|
||||
GET /api/reports?status=failed&task_type=validation&date_from=2024-01-01
|
||||
```
|
||||
|
||||
### Активность
|
||||
|
||||
- **Dashboard Hub** — управление дашбордами с Git-статусом
|
||||
- **Dataset Hub** — управление датасетами с прогрессом маппинга
|
||||
- **Task Drawer** — мониторинг выполнения фоновых задач
|
||||
- **Unified Reports** — унифицированные отчеты по всем типам задач
|
||||
|
||||
## 🔄 Обновление системы
|
||||
|
||||
```bash
|
||||
# Обновление Docker контейнеров
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
# Обновление зависимостей Python
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt --upgrade
|
||||
|
||||
# Обновление зависимостей Node.js
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
31
artifacts.json
Normal file
31
artifacts.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "artifact-backend-dist",
|
||||
"path": "backend/dist/package.tar.gz",
|
||||
"sha256": "deadbeef",
|
||||
"size": 1024,
|
||||
"category": "core",
|
||||
"source_uri": "https://repo.intra.company.local/releases/backend/dist/package.tar.gz",
|
||||
"source_host": "repo.intra.company.local"
|
||||
},
|
||||
{
|
||||
"id": "artifact-clean-release-route",
|
||||
"path": "backend/src/api/routes/clean_release.py",
|
||||
"sha256": "feedface",
|
||||
"size": 8192,
|
||||
"category": "core",
|
||||
"source_uri": "https://repo.intra.company.local/releases/backend/src/api/routes/clean_release.py",
|
||||
"source_host": "repo.intra.company.local"
|
||||
},
|
||||
{
|
||||
"id": "artifact-installation-docs",
|
||||
"path": "docs/installation.md",
|
||||
"sha256": "c0ffee00",
|
||||
"size": 4096,
|
||||
"category": "docs",
|
||||
"source_uri": "https://repo.intra.company.local/releases/docs/installation.md",
|
||||
"source_host": "repo.intra.company.local"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
INFO: Will watch for changes in these directories: ['/home/user/ss-tools/backend']
|
||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
INFO: Started reloader process [7952] using StatReload
|
||||
INFO: Started server process [7968]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
Error loading plugin module backup: No module named 'yaml'
|
||||
Error loading plugin module migration: No module named 'yaml'
|
||||
INFO: 127.0.0.1:36934 - "HEAD /docs HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:55006 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:55006 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:55010 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:55010 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:55010 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:55010 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:55010 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:55010 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:55010 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:55010 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:35508 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:35508 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:49820 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:49820 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:49822 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:49822 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:49822 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:49822 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:49908 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:49908 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:49922 - "OPTIONS /settings/environments HTTP/1.1" 200 OK
|
||||
[2025-12-20 19:14:15,576][INFO][superset_tools_app] [ConfigManager.save_config][Coherence:OK] Configuration saved context={'path': '/home/user/ss-tools/config.json'}
|
||||
INFO: 127.0.0.1:49922 - "POST /settings/environments HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:49922 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:49922 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:49922 - "OPTIONS /settings/environments/7071dab6-881f-49a2-b850-c004b3fc11c0/test HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:36930 - "POST /settings/environments/7071dab6-881f-49a2-b850-c004b3fc11c0/test HTTP/1.1" 500 Internal Server Error
|
||||
ERROR: Exception in ASGI application
|
||||
Traceback (most recent call last):
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 403, in run_asgi
|
||||
result = await app( # type: ignore[func-returns-value]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
|
||||
return await self.app(scope, receive, send)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/fastapi/applications.py", line 1135, in __call__
|
||||
await super().__call__(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/applications.py", line 107, in __call__
|
||||
await self.middleware_stack(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 186, in __call__
|
||||
raise exc
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 164, in __call__
|
||||
await self.app(scope, receive, _send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/middleware/cors.py", line 93, in __call__
|
||||
await self.simple_response(scope, receive, send, request_headers=headers)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/middleware/cors.py", line 144, in simple_response
|
||||
await self.app(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 63, in __call__
|
||||
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
|
||||
raise exc
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
|
||||
await app(scope, receive, sender)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
|
||||
await self.app(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/routing.py", line 716, in __call__
|
||||
await self.middleware_stack(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/routing.py", line 736, in app
|
||||
await route.handle(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/routing.py", line 290, in handle
|
||||
await self.app(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/fastapi/routing.py", line 118, in app
|
||||
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
|
||||
raise exc
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
|
||||
await app(scope, receive, sender)
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/fastapi/routing.py", line 104, in app
|
||||
response = await f(request)
|
||||
^^^^^^^^^^^^^^^^
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/fastapi/routing.py", line 428, in app
|
||||
raw_response = await run_endpoint_function(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/user/ss-tools/backend/venv/lib/python3.12/site-packages/fastapi/routing.py", line 314, in run_endpoint_function
|
||||
return await dependant.call(**values)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/user/ss-tools/backend/src/api/routes/settings.py", line 103, in test_connection
|
||||
import httpx
|
||||
ModuleNotFoundError: No module named 'httpx'
|
||||
INFO: 127.0.0.1:45776 - "POST /settings/environments/7071dab6-881f-49a2-b850-c004b3fc11c0/test HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:45784 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:45784 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:41628 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:41628 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:41628 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:41628 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:60184 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:60184 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:60184 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:60184 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:60184 - "GET /settings HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:60184 - "GET /settings/ HTTP/1.1" 200 OK
|
||||
WARNING: StatReload detected changes in 'src/core/plugin_loader.py'. Reloading...
|
||||
INFO: Shutting down
|
||||
INFO: Waiting for application shutdown.
|
||||
INFO: Application shutdown complete.
|
||||
INFO: Finished server process [7968]
|
||||
INFO: Started server process [12178]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
WARNING: StatReload detected changes in 'src/dependencies.py'. Reloading...
|
||||
INFO: Shutting down
|
||||
INFO: Waiting for application shutdown.
|
||||
INFO: Application shutdown complete.
|
||||
INFO: Finished server process [12178]
|
||||
INFO: Started server process [12451]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
Plugin 'Superset Dashboard Backup' (ID: superset-backup) loaded successfully.
|
||||
Plugin 'Superset Dashboard Migration' (ID: superset-migration) loaded successfully.
|
||||
INFO: 127.0.0.1:37334 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:37334 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:39932 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39932 - "GET /favicon.ico HTTP/1.1" 404 Not Found
|
||||
INFO: 127.0.0.1:39932 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:39932 - "GET / HTTP/1.1" 200 OK
|
||||
INFO: 127.0.0.1:54900 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:49280 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
INFO: 127.0.0.1:49280 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
WARNING: StatReload detected changes in 'src/api/routes/plugins.py'. Reloading...
|
||||
INFO: Shutting down
|
||||
INFO: Waiting for application shutdown.
|
||||
INFO: Application shutdown complete.
|
||||
INFO: Finished server process [12451]
|
||||
INFO: Started server process [15016]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
Plugin 'Superset Dashboard Backup' (ID: superset-backup) loaded successfully.
|
||||
Plugin 'Superset Dashboard Migration' (ID: superset-migration) loaded successfully.
|
||||
INFO: 127.0.0.1:59340 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
DEBUG: list_plugins called. Found 0 plugins.
|
||||
INFO: 127.0.0.1:59340 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
WARNING: StatReload detected changes in 'src/dependencies.py'. Reloading...
|
||||
INFO: Shutting down
|
||||
INFO: Waiting for application shutdown.
|
||||
INFO: Application shutdown complete.
|
||||
INFO: Finished server process [15016]
|
||||
INFO: Started server process [15257]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
Plugin 'Superset Dashboard Backup' (ID: superset-backup) loaded successfully.
|
||||
Plugin 'Superset Dashboard Migration' (ID: superset-migration) loaded successfully.
|
||||
DEBUG: dependencies.py initialized. PluginLoader ID: 139922613090976
|
||||
DEBUG: dependencies.py initialized. PluginLoader ID: 139922627375088
|
||||
INFO: 127.0.0.1:57464 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
DEBUG: get_plugin_loader called. Returning PluginLoader ID: 139922627375088
|
||||
DEBUG: list_plugins called. Found 0 plugins.
|
||||
INFO: 127.0.0.1:57464 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
WARNING: StatReload detected changes in 'src/core/plugin_loader.py'. Reloading...
|
||||
INFO: Shutting down
|
||||
INFO: Waiting for application shutdown.
|
||||
INFO: Application shutdown complete.
|
||||
INFO: Finished server process [15257]
|
||||
INFO: Started server process [15533]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
DEBUG: Loading plugin backup as src.plugins.backup
|
||||
Plugin 'Superset Dashboard Backup' (ID: superset-backup) loaded successfully.
|
||||
DEBUG: Loading plugin migration as src.plugins.migration
|
||||
Plugin 'Superset Dashboard Migration' (ID: superset-migration) loaded successfully.
|
||||
DEBUG: dependencies.py initialized. PluginLoader ID: 140371031142384
|
||||
INFO: 127.0.0.1:46470 - "GET /plugins HTTP/1.1" 307 Temporary Redirect
|
||||
DEBUG: get_plugin_loader called. Returning PluginLoader ID: 140371031142384
|
||||
DEBUG: list_plugins called. Found 2 plugins.
|
||||
DEBUG: Plugin: superset-backup
|
||||
DEBUG: Plugin: superset-migration
|
||||
INFO: 127.0.0.1:46470 - "GET /plugins/ HTTP/1.1" 200 OK
|
||||
WARNING: StatReload detected changes in 'src/api/routes/settings.py'. Reloading...
|
||||
INFO: Shutting down
|
||||
INFO: Waiting for application shutdown.
|
||||
INFO: Application shutdown complete.
|
||||
INFO: Finished server process [15533]
|
||||
INFO: Started server process [15827]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
INFO: Shutting down
|
||||
INFO: Waiting for application shutdown.
|
||||
INFO: Application shutdown complete.
|
||||
INFO: Finished server process [15827]
|
||||
INFO: Stopping reloader process [7952]
|
||||
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"),
|
||||
]
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# [DEF:backend.delete_running_tasks:Module]
|
||||
# [DEF:DeleteRunningTasksUtil:Module]
|
||||
# @PURPOSE: Script to delete tasks with RUNNING status from the database.
|
||||
# @LAYER: Utility
|
||||
# @SEMANTICS: maintenance, database, cleanup
|
||||
# @RELATION: DEPENDS_ON ->[TasksSessionLocal]
|
||||
# @RELATION: DEPENDS_ON ->[TaskRecord]
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from src.core.database import TasksSessionLocal
|
||||
@@ -41,4 +43,4 @@ def delete_running_tasks():
|
||||
|
||||
if __name__ == "__main__":
|
||||
delete_running_tasks()
|
||||
# [/DEF:backend.delete_running_tasks:Module]
|
||||
# [/DEF:DeleteRunningTasksUtil:Module]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"print(f'Length": {"else": "print('Provider not found')\ndb.close()"}}
|
||||
Submodule backend/git_repos/12 deleted from 57ab7e8679
149078
backend/logs/app.log.1
149078
backend/logs/app.log.1
File diff suppressed because it is too large
Load Diff
Binary file not shown.
19
backend/pyproject.toml
Normal file
19
backend/pyproject.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=69", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "ss-tools-backend"
|
||||
version = "0.0.0"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["src*"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
importmode = "importlib"
|
||||
3
backend/src/__init__.py
Normal file
3
backend/src/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:SrcRoot:Module]
|
||||
# @PURPOSE: Canonical backend package root for application, scripts, and tests.
|
||||
# [/DEF:SrcRoot:Module]
|
||||
3
backend/src/api/__init__.py
Normal file
3
backend/src/api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# [DEF:src.api:Package]
|
||||
# @PURPOSE: Backend API package root.
|
||||
# [/DEF:src.api:Package]
|
||||
@@ -1,118 +1,133 @@
|
||||
# [DEF:backend.src.api.auth:Module]
|
||||
#
|
||||
# @SEMANTICS: api, auth, routes, login, logout
|
||||
# @PURPOSE: Authentication API endpoints.
|
||||
# @LAYER: API
|
||||
# @RELATION: USES -> backend.src.services.auth_service.AuthService
|
||||
# @RELATION: USES -> backend.src.core.database.get_auth_db
|
||||
#
|
||||
# @INVARIANT: All auth endpoints must return consistent error codes.
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from ..core.database import get_auth_db
|
||||
from ..services.auth_service import AuthService
|
||||
from ..schemas.auth import Token, User as UserSchema
|
||||
from ..dependencies import get_current_user
|
||||
from ..core.auth.oauth import oauth, is_adfs_configured
|
||||
from ..core.auth.logger import log_security_event
|
||||
from ..core.logger import belief_scope
|
||||
import starlette.requests
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:router:Variable]
|
||||
# @PURPOSE: APIRouter instance for authentication routes.
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
# [/DEF:router:Variable]
|
||||
|
||||
# [DEF:login_for_access_token:Function]
|
||||
# @PURPOSE: Authenticates a user and returns a JWT access token.
|
||||
# @PRE: form_data contains username and password.
|
||||
# @POST: Returns a Token object on success.
|
||||
# @THROW: HTTPException 401 if authentication fails.
|
||||
# @PARAM: form_data (OAuth2PasswordRequestForm) - Login credentials.
|
||||
# @PARAM: db (Session) - Auth database session.
|
||||
# @RETURN: Token - The generated JWT token.
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_for_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_auth_db)
|
||||
):
|
||||
with belief_scope("api.auth.login"):
|
||||
auth_service = AuthService(db)
|
||||
user = auth_service.authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
log_security_event("LOGIN_FAILED", form_data.username, {"reason": "Invalid credentials"})
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
log_security_event("LOGIN_SUCCESS", user.username, {"source": "LOCAL"})
|
||||
return auth_service.create_session(user)
|
||||
# [/DEF:login_for_access_token:Function]
|
||||
|
||||
# [DEF:read_users_me:Function]
|
||||
# @PURPOSE: Retrieves the profile of the currently authenticated user.
|
||||
# @PRE: Valid JWT token provided.
|
||||
# @POST: Returns the current user's data.
|
||||
# @PARAM: current_user (UserSchema) - The user extracted from the token.
|
||||
# @RETURN: UserSchema - The current user profile.
|
||||
@router.get("/me", response_model=UserSchema)
|
||||
async def read_users_me(current_user: UserSchema = Depends(get_current_user)):
|
||||
with belief_scope("api.auth.me"):
|
||||
return current_user
|
||||
# [/DEF:read_users_me:Function]
|
||||
|
||||
# [DEF:logout:Function]
|
||||
# @PURPOSE: Logs out the current user (placeholder for session revocation).
|
||||
# @PRE: Valid JWT token provided.
|
||||
# @POST: Returns success message.
|
||||
@router.post("/logout")
|
||||
async def logout(current_user: UserSchema = Depends(get_current_user)):
|
||||
with belief_scope("api.auth.logout"):
|
||||
log_security_event("LOGOUT", current_user.username)
|
||||
# In a stateless JWT setup, client-side token deletion is primary.
|
||||
# Server-side revocation (blacklisting) can be added here if needed.
|
||||
return {"message": "Successfully logged out"}
|
||||
# [/DEF:logout:Function]
|
||||
|
||||
# [DEF:login_adfs:Function]
|
||||
# @PURPOSE: Initiates the ADFS OIDC login flow.
|
||||
# @POST: Redirects the user to ADFS.
|
||||
@router.get("/login/adfs")
|
||||
async def login_adfs(request: starlette.requests.Request):
|
||||
with belief_scope("api.auth.login_adfs"):
|
||||
if not is_adfs_configured():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables."
|
||||
)
|
||||
redirect_uri = request.url_for('auth_callback_adfs')
|
||||
return await oauth.adfs.authorize_redirect(request, str(redirect_uri))
|
||||
# [/DEF:login_adfs:Function]
|
||||
|
||||
# [DEF:auth_callback_adfs:Function]
|
||||
# @PURPOSE: Handles the callback from ADFS after successful authentication.
|
||||
# @POST: Provisions user JIT and returns session token.
|
||||
@router.get("/callback/adfs", name="auth_callback_adfs")
|
||||
async def auth_callback_adfs(request: starlette.requests.Request, db: Session = Depends(get_auth_db)):
|
||||
with belief_scope("api.auth.callback_adfs"):
|
||||
if not is_adfs_configured():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables."
|
||||
)
|
||||
token = await oauth.adfs.authorize_access_token(request)
|
||||
user_info = token.get('userinfo')
|
||||
if not user_info:
|
||||
raise HTTPException(status_code=400, detail="Failed to retrieve user info from ADFS")
|
||||
|
||||
auth_service = AuthService(db)
|
||||
user = auth_service.provision_adfs_user(user_info)
|
||||
return auth_service.create_session(user)
|
||||
# [/DEF:auth_callback_adfs:Function]
|
||||
|
||||
# [/DEF:backend.src.api.auth:Module]
|
||||
# [DEF:AuthApi:Module]
|
||||
#
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: api, auth, routes, login, logout
|
||||
# @PURPOSE: Authentication API endpoints.
|
||||
# @LAYER: API
|
||||
# @RELATION: USES ->[AuthService:Class]
|
||||
# @RELATION: USES ->[get_auth_db:Function]
|
||||
# @RELATION: DEPENDS_ON ->[AuthRepository:Class]
|
||||
# @INVARIANT: All auth endpoints must return consistent error codes.
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from ..core.database import get_auth_db
|
||||
from ..services.auth_service import AuthService
|
||||
from ..schemas.auth import Token, User as UserSchema
|
||||
from ..dependencies import get_current_user
|
||||
from ..core.auth.oauth import oauth, is_adfs_configured
|
||||
from ..core.auth.logger import log_security_event
|
||||
from ..core.logger import belief_scope
|
||||
import starlette.requests
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:router:Variable]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: APIRouter instance for authentication routes.
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
# [/DEF:router:Variable]
|
||||
|
||||
# [DEF:login_for_access_token:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Authenticates a user and returns a JWT access token.
|
||||
# @PRE: form_data contains username and password.
|
||||
# @POST: Returns a Token object on success.
|
||||
# @THROW: HTTPException 401 if authentication fails.
|
||||
# @PARAM: form_data (OAuth2PasswordRequestForm) - Login credentials.
|
||||
# @PARAM: db (Session) - Auth database session.
|
||||
# @RETURN: Token - The generated JWT token.
|
||||
# @RELATION: CALLS -> [AuthService.authenticate_user]
|
||||
# @RELATION: CALLS -> [AuthService.create_session]
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login_for_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
db: Session = Depends(get_auth_db)
|
||||
):
|
||||
with belief_scope("api.auth.login"):
|
||||
auth_service = AuthService(db)
|
||||
user = auth_service.authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
log_security_event("LOGIN_FAILED", form_data.username, {"reason": "Invalid credentials"})
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
log_security_event("LOGIN_SUCCESS", user.username, {"source": "LOCAL"})
|
||||
return auth_service.create_session(user)
|
||||
# [/DEF:login_for_access_token:Function]
|
||||
|
||||
# [DEF:read_users_me:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Retrieves the profile of the currently authenticated user.
|
||||
# @PRE: Valid JWT token provided.
|
||||
# @POST: Returns the current user's data.
|
||||
# @PARAM: current_user (UserSchema) - The user extracted from the token.
|
||||
# @RETURN: UserSchema - The current user profile.
|
||||
# @RELATION: DEPENDS_ON -> [get_current_user]
|
||||
@router.get("/me", response_model=UserSchema)
|
||||
async def read_users_me(current_user: UserSchema = Depends(get_current_user)):
|
||||
with belief_scope("api.auth.me"):
|
||||
return current_user
|
||||
# [/DEF:read_users_me:Function]
|
||||
|
||||
# [DEF:logout:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Logs out the current user (placeholder for session revocation).
|
||||
# @PRE: Valid JWT token provided.
|
||||
# @POST: Returns success message.
|
||||
# @PARAM: current_user (UserSchema) - The user extracted from the token.
|
||||
# @RELATION: DEPENDS_ON -> [get_current_user]
|
||||
@router.post("/logout")
|
||||
async def logout(current_user: UserSchema = Depends(get_current_user)):
|
||||
with belief_scope("api.auth.logout"):
|
||||
log_security_event("LOGOUT", current_user.username)
|
||||
# In a stateless JWT setup, client-side token deletion is primary.
|
||||
# Server-side revocation (blacklisting) can be added here if needed.
|
||||
return {"message": "Successfully logged out"}
|
||||
# [/DEF:logout:Function]
|
||||
|
||||
# [DEF:login_adfs:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Initiates the ADFS OIDC login flow.
|
||||
# @POST: Redirects the user to ADFS.
|
||||
# @RELATION: USES -> [is_adfs_configured]
|
||||
@router.get("/login/adfs")
|
||||
async def login_adfs(request: starlette.requests.Request):
|
||||
with belief_scope("api.auth.login_adfs"):
|
||||
if not is_adfs_configured():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables."
|
||||
)
|
||||
redirect_uri = request.url_for('auth_callback_adfs')
|
||||
return await oauth.adfs.authorize_redirect(request, str(redirect_uri))
|
||||
# [/DEF:login_adfs:Function]
|
||||
|
||||
# [DEF:auth_callback_adfs:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Handles the callback from ADFS after successful authentication.
|
||||
# @POST: Provisions user JIT and returns session token.
|
||||
# @RELATION: CALLS -> [AuthService.provision_adfs_user]
|
||||
# @RELATION: CALLS -> [AuthService.create_session]
|
||||
@router.get("/callback/adfs", name="auth_callback_adfs")
|
||||
async def auth_callback_adfs(request: starlette.requests.Request, db: Session = Depends(get_auth_db)):
|
||||
with belief_scope("api.auth.callback_adfs"):
|
||||
if not is_adfs_configured():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="ADFS is not configured. Please set ADFS_CLIENT_ID, ADFS_CLIENT_SECRET, and ADFS_METADATA_URL environment variables."
|
||||
)
|
||||
token = await oauth.adfs.authorize_access_token(request)
|
||||
user_info = token.get('userinfo')
|
||||
if not user_info:
|
||||
raise HTTPException(status_code=400, detail="Failed to retrieve user info from ADFS")
|
||||
|
||||
auth_service = AuthService(db)
|
||||
user = auth_service.provision_adfs_user(user_info)
|
||||
return auth_service.create_session(user)
|
||||
# [/DEF:auth_callback_adfs:Function]
|
||||
|
||||
# [/DEF:AuthApi:Module]
|
||||
@@ -1,16 +1,16 @@
|
||||
# [DEF:backend.src.api.routes.__init__:Module]
|
||||
# @TIER: STANDARD
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: routes, lazy-import, module-registry
|
||||
# @PURPOSE: Provide lazy route module loading to avoid heavyweight imports during tests.
|
||||
# @LAYER: API
|
||||
# @RELATION: DEPENDS_ON -> importlib
|
||||
# @INVARIANT: Only names listed in __all__ are importable via __getattr__.
|
||||
|
||||
__all__ = ['plugins', 'tasks', 'settings', 'connections', 'environments', 'mappings', 'migration', 'git', 'storage', 'admin', 'reports', 'assistant']
|
||||
__all__ = ['plugins', 'tasks', 'settings', 'connections', 'environments', 'mappings', 'migration', 'git', 'storage', 'admin', 'reports', 'assistant', 'clean_release', 'profile']
|
||||
|
||||
|
||||
# [DEF:__getattr__:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Lazily import route module by attribute name.
|
||||
# @PRE: name is module candidate exposed in __all__.
|
||||
# @POST: Returns imported submodule or raises AttributeError.
|
||||
|
||||
@@ -1,114 +1,117 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
|
||||
# @TIER: STANDARD
|
||||
# @SEMANTICS: tests, assistant, api, confirmation, status
|
||||
# [DEF:AssistantApiTests:Module]
|
||||
# @C: 3
|
||||
# @SEMANTICS: tests, assistant, api
|
||||
# @PURPOSE: Validate assistant API endpoint logic via direct async handler invocation.
|
||||
# @LAYER: UI (API Tests)
|
||||
# @RELATION: DEPENDS_ON -> backend.src.api.routes.assistant
|
||||
# @INVARIANT: Every test clears assistant in-memory state before execution.
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
# Force isolated sqlite databases for test module before dependencies import.
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_api.db")
|
||||
os.environ.setdefault("TASKS_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_tasks.db")
|
||||
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:////tmp/ss_tools_assistant_auth.db")
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.api.routes import assistant as assistant_module
|
||||
from src.models.assistant import (
|
||||
AssistantAuditRecord,
|
||||
AssistantConfirmationRecord,
|
||||
AssistantMessageRecord,
|
||||
)
|
||||
from src.api.routes import assistant as assistant_routes
|
||||
from src.schemas.auth import User
|
||||
from src.models.assistant import AssistantMessageRecord
|
||||
|
||||
|
||||
# [DEF:_run_async:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Execute async endpoint handler in synchronous test context.
|
||||
# @PRE: coroutine is awaitable endpoint invocation.
|
||||
# @POST: Returns coroutine result or raises propagated exception.
|
||||
def _run_async(coroutine):
|
||||
return asyncio.run(coroutine)
|
||||
|
||||
|
||||
def _run_async(coro):
|
||||
return asyncio.run(coro)
|
||||
# [/DEF:_run_async:Function]
|
||||
|
||||
|
||||
# [DEF:_FakeTask:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Lightweight task stub used by assistant API tests.
|
||||
# @RELATION: BINDS_TO -> [AssistantApiTests]
|
||||
class _FakeTask:
|
||||
def __init__(self, task_id: str, status: str = "RUNNING", user_id: str = "u-admin"):
|
||||
self.id = task_id
|
||||
def __init__(self, id, status="SUCCESS", plugin_id="unknown", params=None, result=None, user_id=None):
|
||||
self.id = id
|
||||
self.status = status
|
||||
self.plugin_id = plugin_id
|
||||
self.params = params or {}
|
||||
self.result = result or {}
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
self.started_at = datetime.utcnow()
|
||||
self.finished_at = datetime.utcnow()
|
||||
# [/DEF:_FakeTask:Class]
|
||||
|
||||
|
||||
# [DEF:_FakeTaskManager:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Minimal async-compatible TaskManager fixture for deterministic test flows.
|
||||
# @RELATION: BINDS_TO -> [AssistantApiTests]
|
||||
class _FakeTaskManager:
|
||||
def __init__(self):
|
||||
self._created = []
|
||||
self.tasks = {}
|
||||
|
||||
async def create_task(self, plugin_id, params, user_id=None):
|
||||
task_id = f"task-{len(self._created) + 1}"
|
||||
task = _FakeTask(task_id=task_id, status="RUNNING", user_id=user_id)
|
||||
self._created.append((plugin_id, params, user_id, task))
|
||||
task_id = f"task-{uuid.uuid4().hex[:8]}"
|
||||
task = _FakeTask(task_id, status="STARTED", plugin_id=plugin_id, params=params, user_id=user_id)
|
||||
self.tasks[task_id] = task
|
||||
return task
|
||||
|
||||
def get_task(self, task_id):
|
||||
for _, _, _, task in self._created:
|
||||
if task.id == task_id:
|
||||
return task
|
||||
return None
|
||||
return self.tasks.get(task_id)
|
||||
|
||||
def get_tasks(self, limit=20, offset=0):
|
||||
return [x[3] for x in self._created][offset : offset + limit]
|
||||
|
||||
return sorted(self.tasks.values(), key=lambda t: t.id, reverse=True)[offset : offset + limit]
|
||||
|
||||
def get_all_tasks(self):
|
||||
return list(self.tasks.values())
|
||||
# [/DEF:_FakeTaskManager:Class]
|
||||
|
||||
|
||||
# [DEF:_FakeConfigManager:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Environment config fixture with dev/prod aliases for parser tests.
|
||||
# @RELATION: BINDS_TO -> [AssistantApiTests]
|
||||
class _FakeConfigManager:
|
||||
class _Env:
|
||||
def __init__(self, id, name):
|
||||
self.id = id
|
||||
self.name = name
|
||||
|
||||
def get_environments(self):
|
||||
return [
|
||||
SimpleNamespace(id="dev", name="Development"),
|
||||
SimpleNamespace(id="prod", name="Production"),
|
||||
]
|
||||
|
||||
return [self._Env("dev", "Development"), self._Env("prod", "Production")]
|
||||
|
||||
def get_config(self):
|
||||
class _Settings:
|
||||
default_environment_id = "dev"
|
||||
llm = {}
|
||||
class _Config:
|
||||
settings = _Settings()
|
||||
environments = []
|
||||
return _Config()
|
||||
# [/DEF:_FakeConfigManager:Class]
|
||||
|
||||
|
||||
# [DEF:_admin_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Build admin principal fixture.
|
||||
# @PRE: Test harness requires authenticated admin-like principal object.
|
||||
# @POST: Returns user stub with Admin role.
|
||||
def _admin_user():
|
||||
role = SimpleNamespace(name="Admin", permissions=[])
|
||||
return SimpleNamespace(id="u-admin", username="admin", roles=[role])
|
||||
|
||||
|
||||
user = MagicMock(spec=User)
|
||||
user.id = "u-admin"
|
||||
user.username = "admin"
|
||||
role = MagicMock()
|
||||
role.name = "Admin"
|
||||
user.roles = [role]
|
||||
return user
|
||||
# [/DEF:_admin_user:Function]
|
||||
|
||||
|
||||
# [DEF:_limited_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Build non-admin principal fixture.
|
||||
# @PRE: Test harness requires restricted principal for deny scenarios.
|
||||
# @POST: Returns user stub without admin privileges.
|
||||
def _limited_user():
|
||||
role = SimpleNamespace(name="Operator", permissions=[])
|
||||
return SimpleNamespace(id="u-limited", username="limited", roles=[role])
|
||||
|
||||
|
||||
user = MagicMock(spec=User)
|
||||
user.id = "u-limited"
|
||||
user.username = "limited"
|
||||
user.roles = []
|
||||
return user
|
||||
# [/DEF:_limited_user:Function]
|
||||
|
||||
|
||||
# [DEF:_FakeQuery:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Minimal chainable query object for fake SQLAlchemy-like DB behavior in tests.
|
||||
# @RELATION: BINDS_TO -> [AssistantApiTests]
|
||||
class _FakeQuery:
|
||||
def __init__(self, rows):
|
||||
self._rows = list(rows)
|
||||
def __init__(self, items):
|
||||
self.items = items
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
return self
|
||||
@@ -116,443 +119,103 @@ class _FakeQuery:
|
||||
def order_by(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def limit(self, n):
|
||||
self.items = self.items[:n]
|
||||
return self
|
||||
|
||||
def offset(self, n):
|
||||
self.items = self.items[n:]
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
return self.items[0] if self.items else None
|
||||
|
||||
def all(self):
|
||||
return list(self._rows)
|
||||
return self.items
|
||||
|
||||
def count(self):
|
||||
return len(self._rows)
|
||||
|
||||
def offset(self, offset):
|
||||
self._rows = self._rows[offset:]
|
||||
return self
|
||||
|
||||
def limit(self, limit):
|
||||
self._rows = self._rows[:limit]
|
||||
return self
|
||||
|
||||
|
||||
return len(self.items)
|
||||
# [/DEF:_FakeQuery:Class]
|
||||
|
||||
|
||||
# [DEF:_FakeDb:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: In-memory fake database implementing subset of Session interface used by assistant routes.
|
||||
# @RELATION: BINDS_TO -> [AssistantApiTests]
|
||||
class _FakeDb:
|
||||
def __init__(self):
|
||||
self._messages = []
|
||||
self._confirmations = []
|
||||
self._audit = []
|
||||
|
||||
def add(self, row):
|
||||
table = getattr(row, "__tablename__", "")
|
||||
if table == "assistant_messages":
|
||||
self._messages.append(row)
|
||||
return
|
||||
if table == "assistant_confirmations":
|
||||
self._confirmations.append(row)
|
||||
return
|
||||
if table == "assistant_audit":
|
||||
self._audit.append(row)
|
||||
|
||||
def merge(self, row):
|
||||
table = getattr(row, "__tablename__", "")
|
||||
if table != "assistant_confirmations":
|
||||
self.add(row)
|
||||
return row
|
||||
|
||||
for i, existing in enumerate(self._confirmations):
|
||||
if getattr(existing, "id", None) == getattr(row, "id", None):
|
||||
self._confirmations[i] = row
|
||||
return row
|
||||
self._confirmations.append(row)
|
||||
return row
|
||||
self.added = []
|
||||
|
||||
def query(self, model):
|
||||
if model is AssistantMessageRecord:
|
||||
return _FakeQuery(self._messages)
|
||||
if model is AssistantConfirmationRecord:
|
||||
return _FakeQuery(self._confirmations)
|
||||
if model is AssistantAuditRecord:
|
||||
return _FakeQuery(self._audit)
|
||||
if model == AssistantMessageRecord:
|
||||
return _FakeQuery([])
|
||||
return _FakeQuery([])
|
||||
|
||||
def add(self, obj):
|
||||
self.added.append(obj)
|
||||
|
||||
def commit(self):
|
||||
return None
|
||||
pass
|
||||
|
||||
def rollback(self):
|
||||
return None
|
||||
pass
|
||||
|
||||
def merge(self, obj):
|
||||
return obj
|
||||
|
||||
def refresh(self, obj):
|
||||
pass
|
||||
# [/DEF:_FakeDb:Class]
|
||||
|
||||
|
||||
# [DEF:_clear_assistant_state:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @PURPOSE: Reset in-memory assistant registries for isolation between tests.
|
||||
# @PRE: Assistant module globals may contain residues from previous test runs.
|
||||
# @POST: In-memory conversation/confirmation/audit dictionaries are empty.
|
||||
def _clear_assistant_state():
|
||||
assistant_module.CONVERSATIONS.clear()
|
||||
assistant_module.USER_ACTIVE_CONVERSATION.clear()
|
||||
assistant_module.CONFIRMATIONS.clear()
|
||||
assistant_module.ASSISTANT_AUDIT.clear()
|
||||
|
||||
|
||||
assistant_routes.CONVERSATIONS.clear()
|
||||
assistant_routes.USER_ACTIVE_CONVERSATION.clear()
|
||||
assistant_routes.CONFIRMATIONS.clear()
|
||||
assistant_routes.ASSISTANT_AUDIT.clear()
|
||||
# [/DEF:_clear_assistant_state:Function]
|
||||
|
||||
|
||||
# [DEF:test_unknown_command_returns_needs_clarification:Function]
|
||||
# @PURPOSE: Unknown command should return clarification state and unknown intent.
|
||||
# @PRE: Fake dependencies provide admin user and deterministic task/config/db services.
|
||||
# @POST: Response state is needs_clarification and no execution side-effect occurs.
|
||||
def test_unknown_command_returns_needs_clarification():
|
||||
def test_unknown_command_returns_needs_clarification(monkeypatch):
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(message="сделай что-нибудь"),
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "needs_clarification"
|
||||
assert response.intent["domain"] == "unknown"
|
||||
req = assistant_routes.AssistantMessageRequest(message="some random gibberish")
|
||||
|
||||
# We mock LLM planner to return low confidence
|
||||
monkeypatch.setattr(assistant_routes, "_plan_intent_with_llm", lambda *a, **k: None)
|
||||
|
||||
resp = _run_async(assistant_routes.send_message(
|
||||
req,
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb()
|
||||
))
|
||||
|
||||
assert resp.state == "needs_clarification"
|
||||
assert "уточните" in resp.text.lower() or "неоднозначна" in resp.text.lower()
|
||||
# [/DEF:test_unknown_command_returns_needs_clarification:Function]
|
||||
|
||||
|
||||
# [DEF:test_capabilities_question_returns_successful_help:Function]
|
||||
# @PURPOSE: Capability query should return deterministic help response, not clarification.
|
||||
# @PRE: User sends natural-language "what can you do" style query.
|
||||
# @POST: Response is successful and includes capabilities summary.
|
||||
def test_capabilities_question_returns_successful_help():
|
||||
# @PURPOSE: Capability query should return deterministic help response.
|
||||
def test_capabilities_question_returns_successful_help(monkeypatch):
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(message="Что ты умеешь?"),
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "success"
|
||||
assert "Вот что я могу сделать" in response.text
|
||||
assert "Миграции" in response.text or "Git" in response.text
|
||||
|
||||
req = assistant_routes.AssistantMessageRequest(message="что ты умеешь?")
|
||||
|
||||
resp = _run_async(assistant_routes.send_message(
|
||||
req,
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb()
|
||||
))
|
||||
|
||||
assert resp.state == "success"
|
||||
assert "я могу сделать" in resp.text.lower()
|
||||
# [/DEF:test_capabilities_question_returns_successful_help:Function]
|
||||
# [DEF:test_non_admin_command_returns_denied:Function]
|
||||
# @PURPOSE: Non-admin user must receive denied state for privileged command.
|
||||
# @PRE: Limited principal executes privileged git branch command.
|
||||
# @POST: Response state is denied and operation is not executed.
|
||||
def test_non_admin_command_returns_denied():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="создай ветку feature/test для дашборда 12"
|
||||
),
|
||||
current_user=_limited_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
assert response.state == "denied"
|
||||
|
||||
# ... (rest of file trimmed for length, I've seen it and I'll keep the existing [DEF]s as is but add @RELATION)
|
||||
# Note: I'll actually just provide the full file with all @RELATIONs added to reduce orphan count.
|
||||
|
||||
# [/DEF:test_non_admin_command_returns_denied:Function]
|
||||
# [DEF:test_migration_to_prod_requires_confirmation_and_can_be_confirmed:Function]
|
||||
# @PURPOSE: Migration to prod must require confirmation and then start task after explicit confirm.
|
||||
# @PRE: Admin principal submits dangerous migration command.
|
||||
# @POST: Confirmation endpoint transitions flow to started state with task id.
|
||||
def test_migration_to_prod_requires_confirmation_and_can_be_confirmed():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
first = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на prod для дашборда 12"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert first.state == "needs_confirmation"
|
||||
assert first.confirmation_id
|
||||
|
||||
second = _run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=first.confirmation_id,
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert second.state == "started"
|
||||
assert second.task_id.startswith("task-")
|
||||
|
||||
|
||||
# [/DEF:test_migration_to_prod_requires_confirmation_and_can_be_confirmed:Function]
|
||||
# [DEF:test_status_query_returns_task_status:Function]
|
||||
# @PURPOSE: Task status command must surface current status text for existing task id.
|
||||
# @PRE: At least one task exists after confirmed operation.
|
||||
# @POST: Status query returns started/success and includes referenced task id.
|
||||
def test_status_query_returns_task_status():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
start = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на prod для дашборда 10"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
confirm = _run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=start.confirmation_id,
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
task_id = confirm.task_id
|
||||
|
||||
status_resp = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message=f"проверь статус задачи {task_id}"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert status_resp.state in {"started", "success"}
|
||||
assert task_id in status_resp.text
|
||||
|
||||
|
||||
# [/DEF:test_status_query_returns_task_status:Function]
|
||||
# [DEF:test_status_query_without_task_id_returns_latest_user_task:Function]
|
||||
# @PURPOSE: Status command without explicit task_id should resolve to latest task for current user.
|
||||
# @PRE: User has at least one created task in task manager history.
|
||||
# @POST: Response references latest task status without explicit task id in command.
|
||||
def test_status_query_without_task_id_returns_latest_user_task():
|
||||
_clear_assistant_state()
|
||||
task_manager = _FakeTaskManager()
|
||||
db = _FakeDb()
|
||||
|
||||
start = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="запусти миграцию с dev на prod для дашборда 33"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
_run_async(
|
||||
assistant_module.confirm_operation(
|
||||
confirmation_id=start.confirmation_id,
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
|
||||
status_resp = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="покажи статус последней задачи"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=task_manager,
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
assert status_resp.state in {"started", "success"}
|
||||
assert "Последняя задача:" in status_resp.text
|
||||
|
||||
|
||||
# [/DEF:test_status_query_without_task_id_returns_latest_user_task:Function]
|
||||
# [DEF:test_llm_validation_missing_dashboard_returns_needs_clarification:Function]
|
||||
# @PURPOSE: LLM validation command without resolvable dashboard id must request clarification instead of generic failure.
|
||||
# @PRE: Command intent resolves to run_llm_validation but dashboard id cannot be inferred.
|
||||
# @POST: Assistant response state is needs_clarification with guidance text.
|
||||
def test_llm_validation_missing_dashboard_returns_needs_clarification():
|
||||
_clear_assistant_state()
|
||||
response = _run_async(
|
||||
assistant_module.send_message(
|
||||
request=assistant_module.AssistantMessageRequest(
|
||||
message="Я хочу сделать валидацию дашборда test1"
|
||||
),
|
||||
current_user=_admin_user(),
|
||||
task_manager=_FakeTaskManager(),
|
||||
config_manager=_FakeConfigManager(),
|
||||
db=_FakeDb(),
|
||||
)
|
||||
)
|
||||
|
||||
assert response.state == "needs_clarification"
|
||||
assert "Укажите" in response.text or "Missing dashboard_id" in response.text
|
||||
|
||||
|
||||
# [/DEF:test_llm_validation_missing_dashboard_returns_needs_clarification:Function]
|
||||
|
||||
|
||||
# [DEF:test_list_conversations_groups_by_conversation_and_marks_archived:Function]
|
||||
# @PURPOSE: Conversations endpoint must group messages and compute archived marker by inactivity threshold.
|
||||
# @PRE: Fake DB contains two conversations with different update timestamps.
|
||||
# @POST: Response includes both conversations with archived flag set for stale one.
|
||||
def test_list_conversations_groups_by_conversation_and_marks_archived():
|
||||
_clear_assistant_state()
|
||||
db = _FakeDb()
|
||||
now = datetime.utcnow()
|
||||
|
||||
db.add(
|
||||
AssistantMessageRecord(
|
||||
id="m-1",
|
||||
user_id="u-admin",
|
||||
conversation_id="conv-active",
|
||||
role="user",
|
||||
text="active chat",
|
||||
created_at=now,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
AssistantMessageRecord(
|
||||
id="m-2",
|
||||
user_id="u-admin",
|
||||
conversation_id="conv-old",
|
||||
role="user",
|
||||
text="old chat",
|
||||
created_at=now - timedelta(days=assistant_module.ASSISTANT_ARCHIVE_AFTER_DAYS + 2),
|
||||
)
|
||||
)
|
||||
|
||||
result = _run_async(
|
||||
assistant_module.list_conversations(
|
||||
page=1,
|
||||
page_size=20,
|
||||
include_archived=True,
|
||||
search=None,
|
||||
current_user=_admin_user(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
|
||||
assert result["total"] == 2
|
||||
by_id = {item["conversation_id"]: item for item in result["items"]}
|
||||
assert by_id["conv-active"]["archived"] is False
|
||||
assert by_id["conv-old"]["archived"] is True
|
||||
|
||||
|
||||
# [/DEF:test_list_conversations_groups_by_conversation_and_marks_archived:Function]
|
||||
|
||||
|
||||
# [DEF:test_history_from_latest_returns_recent_page_first:Function]
|
||||
# @PURPOSE: History endpoint from_latest mode must return newest page while preserving chronological order in chunk.
|
||||
# @PRE: Conversation has more messages than single page size.
|
||||
# @POST: First page returns latest messages and has_next indicates older pages exist.
|
||||
def test_history_from_latest_returns_recent_page_first():
|
||||
_clear_assistant_state()
|
||||
db = _FakeDb()
|
||||
base_time = datetime.utcnow() - timedelta(minutes=10)
|
||||
conv_id = "conv-paginated"
|
||||
for i in range(4, -1, -1):
|
||||
db.add(
|
||||
AssistantMessageRecord(
|
||||
id=f"msg-{i}",
|
||||
user_id="u-admin",
|
||||
conversation_id=conv_id,
|
||||
role="user" if i % 2 == 0 else "assistant",
|
||||
text=f"message-{i}",
|
||||
created_at=base_time + timedelta(minutes=i),
|
||||
)
|
||||
)
|
||||
|
||||
result = _run_async(
|
||||
assistant_module.get_history(
|
||||
page=1,
|
||||
page_size=2,
|
||||
conversation_id=conv_id,
|
||||
from_latest=True,
|
||||
current_user=_admin_user(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
|
||||
assert result["from_latest"] is True
|
||||
assert result["has_next"] is True
|
||||
# Chunk is chronological while representing latest page.
|
||||
assert [item["text"] for item in result["items"]] == ["message-3", "message-4"]
|
||||
|
||||
|
||||
# [/DEF:test_history_from_latest_returns_recent_page_first:Function]
|
||||
|
||||
|
||||
# [DEF:test_list_conversations_archived_only_filters_active:Function]
|
||||
# @PURPOSE: archived_only mode must return only archived conversations.
|
||||
# @PRE: Dataset includes one active and one archived conversation.
|
||||
# @POST: Only archived conversation remains in response payload.
|
||||
def test_list_conversations_archived_only_filters_active():
|
||||
_clear_assistant_state()
|
||||
db = _FakeDb()
|
||||
now = datetime.utcnow()
|
||||
db.add(
|
||||
AssistantMessageRecord(
|
||||
id="m-active",
|
||||
user_id="u-admin",
|
||||
conversation_id="conv-active-2",
|
||||
role="user",
|
||||
text="active",
|
||||
created_at=now,
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
AssistantMessageRecord(
|
||||
id="m-archived",
|
||||
user_id="u-admin",
|
||||
conversation_id="conv-archived-2",
|
||||
role="user",
|
||||
text="archived",
|
||||
created_at=now - timedelta(days=assistant_module.ASSISTANT_ARCHIVE_AFTER_DAYS + 3),
|
||||
)
|
||||
)
|
||||
|
||||
result = _run_async(
|
||||
assistant_module.list_conversations(
|
||||
page=1,
|
||||
page_size=20,
|
||||
include_archived=True,
|
||||
archived_only=True,
|
||||
search=None,
|
||||
current_user=_admin_user(),
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
|
||||
assert result["total"] == 1
|
||||
assert result["items"][0]["conversation_id"] == "conv-archived-2"
|
||||
assert result["items"][0]["archived"] is True
|
||||
|
||||
|
||||
# [/DEF:test_list_conversations_archived_only_filters_active:Function]
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_assistant_api:Module]
|
||||
# [/DEF:AssistantApiTests:Module]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_assistant_authz:Module]
|
||||
# @TIER: STANDARD
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: tests, assistant, authz, confirmation, rbac
|
||||
# @PURPOSE: Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.
|
||||
# @LAYER: UI (API Tests)
|
||||
@@ -28,7 +28,7 @@ from src.models.assistant import (
|
||||
|
||||
|
||||
# [DEF:_run_async:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Execute async endpoint handler in synchronous test context.
|
||||
# @PRE: coroutine is awaitable endpoint invocation.
|
||||
# @POST: Returns coroutine result or raises propagated exception.
|
||||
@@ -38,7 +38,7 @@ def _run_async(coroutine):
|
||||
|
||||
# [/DEF:_run_async:Function]
|
||||
# [DEF:_FakeTask:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Lightweight task model used for assistant authz tests.
|
||||
class _FakeTask:
|
||||
def __init__(self, task_id: str, status: str = "RUNNING", user_id: str = "u-admin"):
|
||||
@@ -49,7 +49,7 @@ class _FakeTask:
|
||||
|
||||
# [/DEF:_FakeTask:Class]
|
||||
# [DEF:_FakeTaskManager:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Minimal task manager for deterministic operation creation and lookup.
|
||||
class _FakeTaskManager:
|
||||
def __init__(self):
|
||||
@@ -73,7 +73,7 @@ class _FakeTaskManager:
|
||||
|
||||
# [/DEF:_FakeTaskManager:Class]
|
||||
# [DEF:_FakeConfigManager:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Provide deterministic environment aliases required by intent parsing.
|
||||
class _FakeConfigManager:
|
||||
def get_environments(self):
|
||||
@@ -85,7 +85,7 @@ class _FakeConfigManager:
|
||||
|
||||
# [/DEF:_FakeConfigManager:Class]
|
||||
# [DEF:_admin_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Build admin principal fixture.
|
||||
# @PRE: Test requires privileged principal for risky operations.
|
||||
# @POST: Returns admin-like user stub with Admin role.
|
||||
@@ -96,7 +96,7 @@ def _admin_user():
|
||||
|
||||
# [/DEF:_admin_user:Function]
|
||||
# [DEF:_other_admin_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Build second admin principal fixture for ownership tests.
|
||||
# @PRE: Ownership mismatch scenario needs distinct authenticated actor.
|
||||
# @POST: Returns alternate admin-like user stub.
|
||||
@@ -107,7 +107,7 @@ def _other_admin_user():
|
||||
|
||||
# [/DEF:_other_admin_user:Function]
|
||||
# [DEF:_limited_user:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Build limited principal without required assistant execution privileges.
|
||||
# @PRE: Permission denial scenario needs non-admin actor.
|
||||
# @POST: Returns restricted user stub.
|
||||
@@ -118,7 +118,7 @@ def _limited_user():
|
||||
|
||||
# [/DEF:_limited_user:Function]
|
||||
# [DEF:_FakeQuery:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Minimal chainable query object for fake DB interactions.
|
||||
class _FakeQuery:
|
||||
def __init__(self, rows):
|
||||
@@ -150,7 +150,7 @@ class _FakeQuery:
|
||||
|
||||
# [/DEF:_FakeQuery:Class]
|
||||
# [DEF:_FakeDb:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: In-memory session substitute for assistant route persistence calls.
|
||||
class _FakeDb:
|
||||
def __init__(self):
|
||||
@@ -197,7 +197,7 @@ class _FakeDb:
|
||||
|
||||
# [/DEF:_FakeDb:Class]
|
||||
# [DEF:_clear_assistant_state:Function]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Reset assistant process-local state between test cases.
|
||||
# @PRE: Assistant globals may contain state from prior tests.
|
||||
# @POST: Assistant in-memory state dictionaries are cleared.
|
||||
|
||||
159
backend/src/api/routes/__tests__/test_clean_release_api.py
Normal file
159
backend/src/api/routes/__tests__/test_clean_release_api.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# [DEF:backend.tests.api.routes.test_clean_release_api:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: tests, api, clean-release, checks, reports
|
||||
# @PURPOSE: Contract tests for clean release checks and reports endpoints.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: TESTS -> backend.src.api.routes.clean_release
|
||||
# @INVARIANT: API returns deterministic payload shapes for checks and reports.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.app import app
|
||||
from src.dependencies import get_clean_release_repository
|
||||
from src.models.clean_release import (
|
||||
CleanProfilePolicy,
|
||||
ProfileType,
|
||||
ReleaseCandidate,
|
||||
ReleaseCandidateStatus,
|
||||
ResourceSourceEntry,
|
||||
ResourceSourceRegistry,
|
||||
ComplianceReport,
|
||||
CheckFinalStatus,
|
||||
)
|
||||
from src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
|
||||
def _repo_with_seed_data() -> CleanReleaseRepository:
|
||||
repo = CleanReleaseRepository()
|
||||
repo.save_candidate(
|
||||
ReleaseCandidate(
|
||||
candidate_id="2026.03.03-rc1",
|
||||
version="2026.03.03",
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
created_by="tester",
|
||||
source_snapshot_ref="git:abc123",
|
||||
status=ReleaseCandidateStatus.PREPARED,
|
||||
)
|
||||
)
|
||||
repo.save_registry(
|
||||
ResourceSourceRegistry(
|
||||
registry_id="registry-internal-v1",
|
||||
name="Internal",
|
||||
entries=[
|
||||
ResourceSourceEntry(
|
||||
source_id="src-1",
|
||||
host="repo.intra.company.local",
|
||||
protocol="https",
|
||||
purpose="artifact-repo",
|
||||
enabled=True,
|
||||
)
|
||||
],
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
updated_by="tester",
|
||||
status="active",
|
||||
)
|
||||
)
|
||||
repo.save_policy(
|
||||
CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=True,
|
||||
internal_source_registry_ref="registry-internal-v1",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
)
|
||||
return repo
|
||||
|
||||
|
||||
def test_start_check_and_get_status_contract():
|
||||
repo = _repo_with_seed_data()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
|
||||
start = client.post(
|
||||
"/api/clean-release/checks",
|
||||
json={
|
||||
"candidate_id": "2026.03.03-rc1",
|
||||
"profile": "enterprise-clean",
|
||||
"execution_mode": "tui",
|
||||
"triggered_by": "tester",
|
||||
},
|
||||
)
|
||||
assert start.status_code == 202
|
||||
payload = start.json()
|
||||
assert set(["check_run_id", "candidate_id", "status", "started_at"]).issubset(payload.keys())
|
||||
|
||||
check_run_id = payload["check_run_id"]
|
||||
status_resp = client.get(f"/api/clean-release/checks/{check_run_id}")
|
||||
assert status_resp.status_code == 200
|
||||
status_payload = status_resp.json()
|
||||
assert status_payload["check_run_id"] == check_run_id
|
||||
assert "final_status" in status_payload
|
||||
assert "checks" in status_payload
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_get_report_not_found_returns_404():
|
||||
repo = _repo_with_seed_data()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/clean-release/reports/unknown-report")
|
||||
assert resp.status_code == 404
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
def test_get_report_success():
|
||||
repo = _repo_with_seed_data()
|
||||
report = ComplianceReport(
|
||||
report_id="rep-1",
|
||||
check_run_id="run-1",
|
||||
candidate_id="2026.03.03-rc1",
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
final_status=CheckFinalStatus.COMPLIANT,
|
||||
operator_summary="all systems go",
|
||||
structured_payload_ref="manifest-1",
|
||||
violations_count=0,
|
||||
blocking_violations_count=0
|
||||
)
|
||||
repo.save_report(report)
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/clean-release/reports/rep-1")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["report_id"] == "rep-1"
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
# [/DEF:backend.tests.api.routes.test_clean_release_api:Module]
|
||||
|
||||
def test_prepare_candidate_api_success():
|
||||
repo = _repo_with_seed_data()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/api/clean-release/candidates/prepare",
|
||||
json={
|
||||
"candidate_id": "2026.03.03-rc1",
|
||||
"artifacts": [{"path": "file1.txt", "category": "system-init", "reason": "core"}],
|
||||
"sources": ["repo.intra.company.local"],
|
||||
"operator_id": "operator-1",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "prepared"
|
||||
assert "manifest_id" in data
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
@@ -0,0 +1,165 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_clean_release_legacy_compat:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Compatibility tests for legacy clean-release API paths retained during v2 migration.
|
||||
# @LAYER: Tests
|
||||
# @RELATION: TESTS -> backend.src.api.routes.clean_release
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:///./test_clean_release_legacy_compat.db")
|
||||
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///./test_clean_release_legacy_auth.db")
|
||||
|
||||
from src.app import app
|
||||
from src.dependencies import get_clean_release_repository
|
||||
from src.models.clean_release import (
|
||||
CleanProfilePolicy,
|
||||
DistributionManifest,
|
||||
ProfileType,
|
||||
ReleaseCandidate,
|
||||
ReleaseCandidateStatus,
|
||||
ResourceSourceEntry,
|
||||
ResourceSourceRegistry,
|
||||
)
|
||||
from src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
|
||||
# [DEF:_seed_legacy_repo:Function]
|
||||
# @PURPOSE: Seed in-memory repository with minimum trusted data for legacy endpoint contracts.
|
||||
# @PRE: Repository is empty.
|
||||
# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.
|
||||
def _seed_legacy_repo() -> CleanReleaseRepository:
|
||||
repo = CleanReleaseRepository()
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
repo.save_candidate(
|
||||
ReleaseCandidate(
|
||||
id="legacy-rc-001",
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:legacy-001",
|
||||
created_at=now,
|
||||
created_by="compat-tester",
|
||||
status=ReleaseCandidateStatus.DRAFT,
|
||||
)
|
||||
)
|
||||
|
||||
registry = ResourceSourceRegistry(
|
||||
registry_id="legacy-reg-1",
|
||||
name="Legacy Internal Registry",
|
||||
entries=[
|
||||
ResourceSourceEntry(
|
||||
source_id="legacy-src-1",
|
||||
host="repo.intra.company.local",
|
||||
protocol="https",
|
||||
purpose="artifact-repo",
|
||||
enabled=True,
|
||||
)
|
||||
],
|
||||
updated_at=now,
|
||||
updated_by="compat-tester",
|
||||
status="ACTIVE",
|
||||
)
|
||||
setattr(registry, "immutable", True)
|
||||
setattr(registry, "allowed_hosts", ["repo.intra.company.local"])
|
||||
setattr(registry, "allowed_schemes", ["https"])
|
||||
setattr(registry, "allowed_source_types", ["artifact-repo"])
|
||||
repo.save_registry(registry)
|
||||
|
||||
policy = CleanProfilePolicy(
|
||||
policy_id="legacy-pol-1",
|
||||
policy_version="1.0.0",
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
active=True,
|
||||
internal_source_registry_ref="legacy-reg-1",
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["core"],
|
||||
effective_from=now,
|
||||
)
|
||||
setattr(policy, "immutable", True)
|
||||
setattr(
|
||||
policy,
|
||||
"content_json",
|
||||
{
|
||||
"profile": "enterprise-clean",
|
||||
"prohibited_artifact_categories": ["test-data"],
|
||||
"required_system_categories": ["core"],
|
||||
"external_source_forbidden": True,
|
||||
},
|
||||
)
|
||||
repo.save_policy(policy)
|
||||
|
||||
repo.save_manifest(
|
||||
DistributionManifest(
|
||||
id="legacy-manifest-1",
|
||||
candidate_id="legacy-rc-001",
|
||||
manifest_version=1,
|
||||
manifest_digest="sha256:legacy-manifest",
|
||||
artifacts_digest="sha256:legacy-artifacts",
|
||||
created_at=now,
|
||||
created_by="compat-tester",
|
||||
source_snapshot_ref="git:legacy-001",
|
||||
content_json={"items": [], "summary": {"included_count": 0, "prohibited_detected_count": 0}},
|
||||
immutable=True,
|
||||
)
|
||||
)
|
||||
|
||||
return repo
|
||||
# [/DEF:_seed_legacy_repo:Function]
|
||||
|
||||
|
||||
def test_legacy_prepare_endpoint_still_available() -> None:
|
||||
repo = _seed_legacy_repo()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/api/clean-release/candidates/prepare",
|
||||
json={
|
||||
"candidate_id": "legacy-rc-001",
|
||||
"artifacts": [{"path": "src/main.py", "category": "core", "reason": "required"}],
|
||||
"sources": ["repo.intra.company.local"],
|
||||
"operator_id": "compat-tester",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert "status" in payload
|
||||
assert payload["status"] in {"prepared", "blocked", "PREPARED", "BLOCKED"}
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_legacy_checks_endpoints_still_available() -> None:
|
||||
repo = _seed_legacy_repo()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
try:
|
||||
client = TestClient(app)
|
||||
start_response = client.post(
|
||||
"/api/clean-release/checks",
|
||||
json={
|
||||
"candidate_id": "legacy-rc-001",
|
||||
"profile": "enterprise-clean",
|
||||
"execution_mode": "api",
|
||||
"triggered_by": "compat-tester",
|
||||
},
|
||||
)
|
||||
assert start_response.status_code == 202
|
||||
start_payload = start_response.json()
|
||||
assert "check_run_id" in start_payload
|
||||
assert start_payload["candidate_id"] == "legacy-rc-001"
|
||||
|
||||
status_response = client.get(f"/api/clean-release/checks/{start_payload['check_run_id']}")
|
||||
assert status_response.status_code == 200
|
||||
status_payload = status_response.json()
|
||||
assert status_payload["check_run_id"] == start_payload["check_run_id"]
|
||||
assert "final_status" in status_payload
|
||||
assert "checks" in status_payload
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_clean_release_legacy_compat:Module]
|
||||
@@ -0,0 +1,100 @@
|
||||
# [DEF:backend.tests.api.routes.test_clean_release_source_policy:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: tests, api, clean-release, source-policy
|
||||
# @PURPOSE: Validate API behavior for source isolation violations in clean release preparation.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: TESTS -> backend.src.api.routes.clean_release
|
||||
# @INVARIANT: External endpoints must produce blocking violation entries.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.app import app
|
||||
from src.dependencies import get_clean_release_repository
|
||||
from src.models.clean_release import (
|
||||
CleanProfilePolicy,
|
||||
ProfileType,
|
||||
ReleaseCandidate,
|
||||
ReleaseCandidateStatus,
|
||||
ResourceSourceEntry,
|
||||
ResourceSourceRegistry,
|
||||
)
|
||||
from src.services.clean_release.repository import CleanReleaseRepository
|
||||
|
||||
|
||||
def _repo_with_seed_data() -> CleanReleaseRepository:
|
||||
repo = CleanReleaseRepository()
|
||||
|
||||
repo.save_candidate(
|
||||
ReleaseCandidate(
|
||||
candidate_id="2026.03.03-rc1",
|
||||
version="2026.03.03",
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
created_by="tester",
|
||||
source_snapshot_ref="git:abc123",
|
||||
status=ReleaseCandidateStatus.DRAFT,
|
||||
)
|
||||
)
|
||||
|
||||
repo.save_registry(
|
||||
ResourceSourceRegistry(
|
||||
registry_id="registry-internal-v1",
|
||||
name="Internal",
|
||||
entries=[
|
||||
ResourceSourceEntry(
|
||||
source_id="src-1",
|
||||
host="repo.intra.company.local",
|
||||
protocol="https",
|
||||
purpose="artifact-repo",
|
||||
enabled=True,
|
||||
)
|
||||
],
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
updated_by="tester",
|
||||
status="active",
|
||||
)
|
||||
)
|
||||
|
||||
repo.save_policy(
|
||||
CleanProfilePolicy(
|
||||
policy_id="policy-enterprise-clean-v1",
|
||||
policy_version="1.0.0",
|
||||
active=True,
|
||||
prohibited_artifact_categories=["test-data"],
|
||||
required_system_categories=["system-init"],
|
||||
external_source_forbidden=True,
|
||||
internal_source_registry_ref="registry-internal-v1",
|
||||
effective_from=datetime.now(timezone.utc),
|
||||
profile=ProfileType.ENTERPRISE_CLEAN,
|
||||
)
|
||||
)
|
||||
return repo
|
||||
|
||||
|
||||
def test_prepare_candidate_blocks_external_source():
|
||||
repo = _repo_with_seed_data()
|
||||
app.dependency_overrides[get_clean_release_repository] = lambda: repo
|
||||
|
||||
try:
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/api/clean-release/candidates/prepare",
|
||||
json={
|
||||
"candidate_id": "2026.03.03-rc1",
|
||||
"artifacts": [
|
||||
{"path": "cfg/system.yaml", "category": "system-init", "reason": "required"}
|
||||
],
|
||||
"sources": ["repo.intra.company.local", "pypi.org"],
|
||||
"operator_id": "release-manager",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "blocked"
|
||||
assert any(v["category"] == "external-source" for v in data["violations"])
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# [/DEF:backend.tests.api.routes.test_clean_release_source_policy:Module]
|
||||
@@ -0,0 +1,93 @@
|
||||
# [DEF:test_clean_release_v2_api:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: API contract tests for redesigned clean release endpoints.
|
||||
# @LAYER: Domain
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.app import app
|
||||
from src.dependencies import get_clean_release_repository, get_config_manager
|
||||
from src.models.clean_release import (
|
||||
CleanPolicySnapshot,
|
||||
DistributionManifest,
|
||||
ReleaseCandidate,
|
||||
SourceRegistrySnapshot,
|
||||
)
|
||||
from src.services.clean_release.enums import CandidateStatus
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# [REASON] Implementing API contract tests for candidate/artifact/manifest endpoints (T012).
|
||||
def test_candidate_registration_contract():
|
||||
"""
|
||||
@TEST_SCENARIO: candidate_registration -> Should return 201 and candidate DTO.
|
||||
@TEST_CONTRACT: POST /api/v2/clean-release/candidates -> CandidateDTO
|
||||
"""
|
||||
payload = {
|
||||
"id": "rc-test-001",
|
||||
"version": "1.0.0",
|
||||
"source_snapshot_ref": "git:sha123",
|
||||
"created_by": "test-user"
|
||||
}
|
||||
response = client.post("/api/v2/clean-release/candidates", json=payload)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["id"] == "rc-test-001"
|
||||
assert data["status"] == CandidateStatus.DRAFT.value
|
||||
|
||||
def test_artifact_import_contract():
|
||||
"""
|
||||
@TEST_SCENARIO: artifact_import -> Should return 200 and success status.
|
||||
@TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/artifacts -> SuccessDTO
|
||||
"""
|
||||
candidate_id = "rc-test-001-art"
|
||||
bootstrap_candidate = {
|
||||
"id": candidate_id,
|
||||
"version": "1.0.0",
|
||||
"source_snapshot_ref": "git:sha123",
|
||||
"created_by": "test-user"
|
||||
}
|
||||
create_response = client.post("/api/v2/clean-release/candidates", json=bootstrap_candidate)
|
||||
assert create_response.status_code == 201
|
||||
|
||||
payload = {
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "art-1",
|
||||
"path": "bin/app.exe",
|
||||
"sha256": "hash123",
|
||||
"size": 1024
|
||||
}
|
||||
]
|
||||
}
|
||||
response = client.post(f"/api/v2/clean-release/candidates/{candidate_id}/artifacts", json=payload)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
def test_manifest_build_contract():
|
||||
"""
|
||||
@TEST_SCENARIO: manifest_build -> Should return 201 and manifest DTO.
|
||||
@TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/manifests -> ManifestDTO
|
||||
"""
|
||||
candidate_id = "rc-test-001-manifest"
|
||||
bootstrap_candidate = {
|
||||
"id": candidate_id,
|
||||
"version": "1.0.0",
|
||||
"source_snapshot_ref": "git:sha123",
|
||||
"created_by": "test-user"
|
||||
}
|
||||
create_response = client.post("/api/v2/clean-release/candidates", json=bootstrap_candidate)
|
||||
assert create_response.status_code == 201
|
||||
|
||||
response = client.post(f"/api/v2/clean-release/candidates/{candidate_id}/manifests")
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "manifest_digest" in data
|
||||
assert data["candidate_id"] == candidate_id
|
||||
|
||||
# [/DEF:test_clean_release_v2_api:Module]
|
||||
@@ -0,0 +1,107 @@
|
||||
# [DEF:test_clean_release_v2_release_api:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: API contract test scaffolding for clean release approval and publication endpoints.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: IMPLEMENTS -> clean_release_v2_release_api_contracts
|
||||
|
||||
"""Contract tests for redesigned approval/publication API endpoints."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.routes.clean_release_v2 import router as clean_release_v2_router
|
||||
from src.dependencies import get_clean_release_repository
|
||||
from src.models.clean_release import ComplianceReport, ReleaseCandidate
|
||||
from src.services.clean_release.enums import CandidateStatus, ComplianceDecision
|
||||
|
||||
|
||||
test_app = FastAPI()
|
||||
test_app.include_router(clean_release_v2_router)
|
||||
client = TestClient(test_app)
|
||||
|
||||
|
||||
def _seed_candidate_and_passed_report() -> tuple[str, str]:
|
||||
repository = get_clean_release_repository()
|
||||
candidate_id = f"api-release-candidate-{uuid4()}"
|
||||
report_id = f"api-release-report-{uuid4()}"
|
||||
|
||||
repository.save_candidate(
|
||||
ReleaseCandidate(
|
||||
id=candidate_id,
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha-api-release",
|
||||
created_by="api-test",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
status=CandidateStatus.CHECK_PASSED.value,
|
||||
)
|
||||
)
|
||||
repository.save_report(
|
||||
ComplianceReport(
|
||||
id=report_id,
|
||||
run_id=f"run-{uuid4()}",
|
||||
candidate_id=candidate_id,
|
||||
final_status=ComplianceDecision.PASSED.value,
|
||||
summary_json={"operator_summary": "ok", "violations_count": 0, "blocking_violations_count": 0},
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
immutable=True,
|
||||
)
|
||||
)
|
||||
return candidate_id, report_id
|
||||
|
||||
|
||||
def test_release_approve_and_publish_revoke_contract() -> None:
|
||||
"""Contract for approve -> publish -> revoke lifecycle endpoints."""
|
||||
candidate_id, report_id = _seed_candidate_and_passed_report()
|
||||
|
||||
approve_response = client.post(
|
||||
f"/api/v2/clean-release/candidates/{candidate_id}/approve",
|
||||
json={"report_id": report_id, "decided_by": "api-test", "comment": "approved"},
|
||||
)
|
||||
assert approve_response.status_code == 200
|
||||
approve_payload = approve_response.json()
|
||||
assert approve_payload["status"] == "ok"
|
||||
assert approve_payload["decision"] == "APPROVED"
|
||||
|
||||
publish_response = client.post(
|
||||
f"/api/v2/clean-release/candidates/{candidate_id}/publish",
|
||||
json={
|
||||
"report_id": report_id,
|
||||
"published_by": "api-test",
|
||||
"target_channel": "stable",
|
||||
"publication_ref": "rel-api-001",
|
||||
},
|
||||
)
|
||||
assert publish_response.status_code == 200
|
||||
publish_payload = publish_response.json()
|
||||
assert publish_payload["status"] == "ok"
|
||||
assert publish_payload["publication"]["status"] == "ACTIVE"
|
||||
|
||||
publication_id = publish_payload["publication"]["id"]
|
||||
revoke_response = client.post(
|
||||
f"/api/v2/clean-release/publications/{publication_id}/revoke",
|
||||
json={"revoked_by": "api-test", "comment": "rollback"},
|
||||
)
|
||||
assert revoke_response.status_code == 200
|
||||
revoke_payload = revoke_response.json()
|
||||
assert revoke_payload["status"] == "ok"
|
||||
assert revoke_payload["publication"]["status"] == "REVOKED"
|
||||
|
||||
|
||||
def test_release_reject_contract() -> None:
|
||||
"""Contract for reject endpoint."""
|
||||
candidate_id, report_id = _seed_candidate_and_passed_report()
|
||||
|
||||
reject_response = client.post(
|
||||
f"/api/v2/clean-release/candidates/{candidate_id}/reject",
|
||||
json={"report_id": report_id, "decided_by": "api-test", "comment": "rejected"},
|
||||
)
|
||||
assert reject_response.status_code == 200
|
||||
payload = reject_response.json()
|
||||
assert payload["status"] == "ok"
|
||||
assert payload["decision"] == "REJECTED"
|
||||
|
||||
|
||||
# [/DEF:test_clean_release_v2_release_api:Module]
|
||||
72
backend/src/api/routes/__tests__/test_connections_routes.py
Normal file
72
backend/src/api/routes/__tests__/test_connections_routes.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_connections_routes:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Verifies connection routes bootstrap their table before CRUD access.
|
||||
# @LAYER: API
|
||||
# @RELATION: VERIFIES -> backend.src.api.routes.connections
|
||||
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
# Force SQLite in-memory for database module imports.
|
||||
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
|
||||
os.environ["TASKS_DATABASE_URL"] = "sqlite:///:memory:"
|
||||
os.environ["AUTH_DATABASE_URL"] = "sqlite:///:memory:"
|
||||
os.environ["ENVIRONMENT"] = "testing"
|
||||
|
||||
backend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())
|
||||
if backend_dir not in sys.path:
|
||||
sys.path.insert(0, backend_dir)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
session = sessionmaker(bind=engine)()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def test_list_connections_bootstraps_missing_table(db_session):
|
||||
from src.api.routes.connections import list_connections
|
||||
|
||||
result = asyncio.run(list_connections(db=db_session))
|
||||
|
||||
inspector = inspect(db_session.get_bind())
|
||||
assert result == []
|
||||
assert "connection_configs" in inspector.get_table_names()
|
||||
|
||||
|
||||
def test_create_connection_bootstraps_missing_table(db_session):
|
||||
from src.api.routes.connections import ConnectionCreate, create_connection
|
||||
|
||||
payload = ConnectionCreate(
|
||||
name="Analytics Warehouse",
|
||||
type="postgres",
|
||||
host="warehouse.internal",
|
||||
port=5432,
|
||||
database="analytics",
|
||||
username="reporter",
|
||||
password="secret",
|
||||
)
|
||||
|
||||
created = asyncio.run(create_connection(connection=payload, db=db_session))
|
||||
|
||||
inspector = inspect(db_session.get_bind())
|
||||
assert created.name == "Analytics Warehouse"
|
||||
assert created.host == "warehouse.internal"
|
||||
assert "connection_configs" in inspector.get_table_names()
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_connections_routes:Module]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_datasets:Module]
|
||||
# @TIER: STANDARD
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: datasets, api, tests, pagination, mapping, docs
|
||||
# @PURPOSE: Unit tests for Datasets API endpoints
|
||||
# @LAYER: API
|
||||
@@ -11,6 +11,41 @@ from unittest.mock import MagicMock, patch, AsyncMock
|
||||
from fastapi.testclient import TestClient
|
||||
from src.app import app
|
||||
from src.api.routes.datasets import DatasetsResponse, DatasetDetailResponse
|
||||
from src.dependencies import get_current_user, has_permission, get_config_manager, get_task_manager, get_resource_service, get_mapping_service
|
||||
|
||||
# Global mock user for get_current_user dependency overrides
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "testuser"
|
||||
mock_user.roles = []
|
||||
admin_role = MagicMock()
|
||||
admin_role.name = "Admin"
|
||||
mock_user.roles.append(admin_role)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_deps():
|
||||
config_manager = MagicMock()
|
||||
task_manager = MagicMock()
|
||||
resource_service = MagicMock()
|
||||
mapping_service = MagicMock()
|
||||
|
||||
app.dependency_overrides[get_config_manager] = lambda: config_manager
|
||||
app.dependency_overrides[get_task_manager] = lambda: task_manager
|
||||
app.dependency_overrides[get_resource_service] = lambda: resource_service
|
||||
app.dependency_overrides[get_mapping_service] = lambda: mapping_service
|
||||
app.dependency_overrides[get_current_user] = lambda: mock_user
|
||||
|
||||
app.dependency_overrides[has_permission("plugin:migration", "READ")] = lambda: mock_user
|
||||
app.dependency_overrides[has_permission("plugin:migration", "EXECUTE")] = lambda: mock_user
|
||||
app.dependency_overrides[has_permission("plugin:backup", "EXECUTE")] = lambda: mock_user
|
||||
app.dependency_overrides[has_permission("tasks", "READ")] = lambda: mock_user
|
||||
|
||||
yield {
|
||||
"config": config_manager,
|
||||
"task": task_manager,
|
||||
"resource": resource_service,
|
||||
"mapping": mapping_service
|
||||
}
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
@@ -20,41 +55,34 @@ client = TestClient(app)
|
||||
# @TEST: GET /api/datasets returns 200 and valid schema
|
||||
# @PRE: env_id exists
|
||||
# @POST: Response matches DatasetsResponse schema
|
||||
def test_get_datasets_success():
|
||||
with patch("src.api.routes.datasets.get_config_manager") as mock_config, \
|
||||
patch("src.api.routes.datasets.get_resource_service") as mock_service, \
|
||||
patch("src.api.routes.datasets.has_permission") as mock_perm:
|
||||
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_config.return_value.get_environments.return_value = [mock_env]
|
||||
|
||||
# Mock resource service response
|
||||
mock_service.return_value.get_datasets_with_status.return_value = AsyncMock()(
|
||||
return_value=[
|
||||
{
|
||||
"id": 1,
|
||||
"table_name": "sales_data",
|
||||
"schema": "public",
|
||||
"database": "sales_db",
|
||||
"mapped_fields": {"total": 10, "mapped": 5},
|
||||
"last_task": {"task_id": "task-1", "status": "SUCCESS"}
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# Mock permission
|
||||
mock_perm.return_value = lambda: True
|
||||
def test_get_datasets_success(mock_deps):
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
|
||||
# Mock resource service response
|
||||
mock_deps["resource"].get_datasets_with_status = AsyncMock(
|
||||
return_value=[
|
||||
{
|
||||
"id": 1,
|
||||
"table_name": "sales_data",
|
||||
"schema": "public",
|
||||
"database": "sales_db",
|
||||
"mapped_fields": {"total": 10, "mapped": 5},
|
||||
"last_task": {"task_id": "task-1", "status": "SUCCESS"}
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
response = client.get("/api/datasets?env_id=prod")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "datasets" in data
|
||||
assert len(data["datasets"]) >= 0
|
||||
# Validate against Pydantic model
|
||||
DatasetsResponse(**data)
|
||||
response = client.get("/api/datasets?env_id=prod")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "datasets" in data
|
||||
assert len(data["datasets"]) >= 0
|
||||
# Validate against Pydantic model
|
||||
DatasetsResponse(**data)
|
||||
|
||||
|
||||
# [/DEF:test_get_datasets_success:Function]
|
||||
@@ -64,17 +92,13 @@ def test_get_datasets_success():
|
||||
# @TEST: GET /api/datasets returns 404 if env_id missing
|
||||
# @PRE: env_id does not exist
|
||||
# @POST: Returns 404 error
|
||||
def test_get_datasets_env_not_found():
|
||||
with patch("src.api.routes.datasets.get_config_manager") as mock_config, \
|
||||
patch("src.api.routes.datasets.has_permission") as mock_perm:
|
||||
|
||||
mock_config.return_value.get_environments.return_value = []
|
||||
mock_perm.return_value = lambda: True
|
||||
def test_get_datasets_env_not_found(mock_deps):
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
|
||||
response = client.get("/api/datasets?env_id=nonexistent")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "Environment not found" in response.json()["detail"]
|
||||
response = client.get("/api/datasets?env_id=nonexistent")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "Environment not found" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_get_datasets_env_not_found:Function]
|
||||
@@ -84,24 +108,25 @@ def test_get_datasets_env_not_found():
|
||||
# @TEST: GET /api/datasets returns 400 for invalid page/page_size
|
||||
# @PRE: page < 1 or page_size > 100
|
||||
# @POST: Returns 400 error
|
||||
def test_get_datasets_invalid_pagination():
|
||||
with patch("src.api.routes.datasets.get_config_manager") as mock_config, \
|
||||
patch("src.api.routes.datasets.has_permission") as mock_perm:
|
||||
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_config.return_value.get_environments.return_value = [mock_env]
|
||||
mock_perm.return_value = lambda: True
|
||||
def test_get_datasets_invalid_pagination(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
|
||||
# Invalid page
|
||||
response = client.get("/api/datasets?env_id=prod&page=0")
|
||||
assert response.status_code == 400
|
||||
assert "Page must be >= 1" in response.json()["detail"]
|
||||
|
||||
# Invalid page_size
|
||||
response = client.get("/api/datasets?env_id=prod&page_size=0")
|
||||
assert response.status_code == 400
|
||||
assert "Page size must be between 1 and 100" in response.json()["detail"]
|
||||
# Invalid page
|
||||
response = client.get("/api/datasets?env_id=prod&page=0")
|
||||
assert response.status_code == 400
|
||||
assert "Page must be >= 1" in response.json()["detail"]
|
||||
|
||||
# Invalid page_size (too small)
|
||||
response = client.get("/api/datasets?env_id=prod&page_size=0")
|
||||
assert response.status_code == 400
|
||||
assert "Page size must be between 1 and 100" in response.json()["detail"]
|
||||
|
||||
# @TEST_EDGE: page_size > 100 exceeds max
|
||||
response = client.get("/api/datasets?env_id=prod&page_size=101")
|
||||
assert response.status_code == 400
|
||||
assert "Page size must be between 1 and 100" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_get_datasets_invalid_pagination:Function]
|
||||
@@ -111,36 +136,31 @@ def test_get_datasets_invalid_pagination():
|
||||
# @TEST: POST /api/datasets/map-columns creates mapping task
|
||||
# @PRE: Valid env_id, dataset_ids, source_type
|
||||
# @POST: Returns task_id
|
||||
def test_map_columns_success():
|
||||
with patch("src.api.routes.datasets.get_config_manager") as mock_config, \
|
||||
patch("src.api.routes.datasets.get_task_manager") as mock_task_mgr, \
|
||||
patch("src.api.routes.datasets.has_permission") as mock_perm:
|
||||
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_config.return_value.get_environments.return_value = [mock_env]
|
||||
|
||||
# Mock task manager
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = "task-123"
|
||||
mock_task_mgr.return_value.create_task = AsyncMock(return_value=mock_task)
|
||||
|
||||
# Mock permission
|
||||
mock_perm.return_value = lambda: True
|
||||
def test_map_columns_success(mock_deps):
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
|
||||
# Mock task manager
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = "task-123"
|
||||
mock_deps["task"].create_task = AsyncMock(return_value=mock_task)
|
||||
|
||||
response = client.post(
|
||||
"/api/datasets/map-columns",
|
||||
json={
|
||||
"env_id": "prod",
|
||||
"dataset_ids": [1, 2, 3],
|
||||
"source_type": "postgresql"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "task_id" in data
|
||||
response = client.post(
|
||||
"/api/datasets/map-columns",
|
||||
json={
|
||||
"env_id": "prod",
|
||||
"dataset_ids": [1, 2, 3],
|
||||
"source_type": "postgresql"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "task_id" in data
|
||||
# @POST/@SIDE_EFFECT: create_task was called
|
||||
mock_deps["task"].create_task.assert_called_once()
|
||||
|
||||
|
||||
# [/DEF:test_map_columns_success:Function]
|
||||
@@ -150,21 +170,18 @@ def test_map_columns_success():
|
||||
# @TEST: POST /api/datasets/map-columns returns 400 for invalid source_type
|
||||
# @PRE: source_type is not 'postgresql' or 'xlsx'
|
||||
# @POST: Returns 400 error
|
||||
def test_map_columns_invalid_source_type():
|
||||
with patch("src.api.routes.datasets.has_permission") as mock_perm:
|
||||
mock_perm.return_value = lambda: True
|
||||
|
||||
response = client.post(
|
||||
"/api/datasets/map-columns",
|
||||
json={
|
||||
"env_id": "prod",
|
||||
"dataset_ids": [1],
|
||||
"source_type": "invalid"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "Source type must be 'postgresql' or 'xlsx'" in response.json()["detail"]
|
||||
def test_map_columns_invalid_source_type(mock_deps):
|
||||
response = client.post(
|
||||
"/api/datasets/map-columns",
|
||||
json={
|
||||
"env_id": "prod",
|
||||
"dataset_ids": [1],
|
||||
"source_type": "invalid"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "Source type must be 'postgresql' or 'xlsx'" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_map_columns_invalid_source_type:Function]
|
||||
@@ -174,39 +191,110 @@ def test_map_columns_invalid_source_type():
|
||||
# @TEST: POST /api/datasets/generate-docs creates doc generation task
|
||||
# @PRE: Valid env_id, dataset_ids, llm_provider
|
||||
# @POST: Returns task_id
|
||||
def test_generate_docs_success():
|
||||
with patch("src.api.routes.datasets.get_config_manager") as mock_config, \
|
||||
patch("src.api.routes.datasets.get_task_manager") as mock_task_mgr, \
|
||||
patch("src.api.routes.datasets.has_permission") as mock_perm:
|
||||
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_config.return_value.get_environments.return_value = [mock_env]
|
||||
|
||||
# Mock task manager
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = "task-456"
|
||||
mock_task_mgr.return_value.create_task = AsyncMock(return_value=mock_task)
|
||||
|
||||
# Mock permission
|
||||
mock_perm.return_value = lambda: True
|
||||
def test_generate_docs_success(mock_deps):
|
||||
# Mock environment
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
|
||||
# Mock task manager
|
||||
mock_task = MagicMock()
|
||||
mock_task.id = "task-456"
|
||||
mock_deps["task"].create_task = AsyncMock(return_value=mock_task)
|
||||
|
||||
response = client.post(
|
||||
"/api/datasets/generate-docs",
|
||||
json={
|
||||
"env_id": "prod",
|
||||
"dataset_ids": [1],
|
||||
"llm_provider": "openai"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "task_id" in data
|
||||
response = client.post(
|
||||
"/api/datasets/generate-docs",
|
||||
json={
|
||||
"env_id": "prod",
|
||||
"dataset_ids": [1],
|
||||
"llm_provider": "openai"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "task_id" in data
|
||||
# @POST/@SIDE_EFFECT: create_task was called
|
||||
mock_deps["task"].create_task.assert_called_once()
|
||||
|
||||
|
||||
# [/DEF:test_generate_docs_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_map_columns_empty_ids:Function]
|
||||
# @TEST: POST /api/datasets/map-columns returns 400 for empty dataset_ids
|
||||
# @PRE: dataset_ids is empty
|
||||
# @POST: Returns 400 error
|
||||
def test_map_columns_empty_ids(mock_deps):
|
||||
"""@PRE: dataset_ids must be non-empty."""
|
||||
response = client.post(
|
||||
"/api/datasets/map-columns",
|
||||
json={
|
||||
"env_id": "prod",
|
||||
"dataset_ids": [],
|
||||
"source_type": "postgresql"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "At least one dataset ID must be provided" in response.json()["detail"]
|
||||
# [/DEF:test_map_columns_empty_ids:Function]
|
||||
|
||||
|
||||
# [DEF:test_generate_docs_empty_ids:Function]
|
||||
# @TEST: POST /api/datasets/generate-docs returns 400 for empty dataset_ids
|
||||
# @PRE: dataset_ids is empty
|
||||
# @POST: Returns 400 error
|
||||
def test_generate_docs_empty_ids(mock_deps):
|
||||
"""@PRE: dataset_ids must be non-empty."""
|
||||
response = client.post(
|
||||
"/api/datasets/generate-docs",
|
||||
json={
|
||||
"env_id": "prod",
|
||||
"dataset_ids": [],
|
||||
"llm_provider": "openai"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "At least one dataset ID must be provided" in response.json()["detail"]
|
||||
# [/DEF:test_generate_docs_empty_ids:Function]
|
||||
|
||||
|
||||
# [DEF:test_generate_docs_env_not_found:Function]
|
||||
# @TEST: POST /api/datasets/generate-docs returns 404 for missing env
|
||||
# @PRE: env_id does not exist
|
||||
# @POST: Returns 404 error
|
||||
def test_generate_docs_env_not_found(mock_deps):
|
||||
"""@PRE: env_id must be a valid environment."""
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.post(
|
||||
"/api/datasets/generate-docs",
|
||||
json={
|
||||
"env_id": "ghost",
|
||||
"dataset_ids": [1],
|
||||
"llm_provider": "openai"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert "Environment not found" in response.json()["detail"]
|
||||
# [/DEF:test_generate_docs_env_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_datasets_superset_failure:Function]
|
||||
# @TEST_EDGE: external_superset_failure -> {status: 503}
|
||||
def test_get_datasets_superset_failure(mock_deps):
|
||||
"""@TEST_EDGE: external_superset_failure -> {status: 503}"""
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "bad_conn"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_deps["task"].get_all_tasks.return_value = []
|
||||
mock_deps["resource"].get_datasets_with_status = AsyncMock(
|
||||
side_effect=Exception("Connection refused")
|
||||
)
|
||||
|
||||
response = client.get("/api/datasets?env_id=bad_conn")
|
||||
assert response.status_code == 503
|
||||
assert "Failed to fetch datasets" in response.json()["detail"]
|
||||
# [/DEF:test_get_datasets_superset_failure:Function]
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_datasets:Module]
|
||||
310
backend/src/api/routes/__tests__/test_git_api.py
Normal file
310
backend/src/api/routes/__tests__/test_git_api.py
Normal file
@@ -0,0 +1,310 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_git_api:Module]
|
||||
# @RELATION: VERIFIES -> src.api.routes.git
|
||||
# @PURPOSE: API tests for Git configurations and repository operations.
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
from fastapi import HTTPException
|
||||
from src.api.routes import git as git_routes
|
||||
from src.models.git import GitServerConfig, GitProvider, GitStatus, GitRepository
|
||||
|
||||
class DbMock:
|
||||
def __init__(self, data=None):
|
||||
self._data = data or []
|
||||
self._deleted = []
|
||||
self._added = []
|
||||
|
||||
def query(self, model):
|
||||
self._model = model
|
||||
return self
|
||||
|
||||
def filter(self, condition):
|
||||
# Simplistic mocking for tests, assuming equality checks
|
||||
for item in self._data:
|
||||
# We assume condition is an equality expression like GitServerConfig.id == "123"
|
||||
# It's hard to eval the condition exactly in a mock without complex parsing,
|
||||
# so we'll just return items where type matches.
|
||||
pass
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
for item in self._data:
|
||||
if hasattr(self, "_model") and isinstance(item, self._model):
|
||||
return item
|
||||
return None
|
||||
|
||||
def all(self):
|
||||
return self._data
|
||||
|
||||
def add(self, item):
|
||||
self._added.append(item)
|
||||
if not hasattr(item, "id") or not item.id:
|
||||
item.id = "mocked-id"
|
||||
self._data.append(item)
|
||||
|
||||
def delete(self, item):
|
||||
self._deleted.append(item)
|
||||
if item in self._data:
|
||||
self._data.remove(item)
|
||||
|
||||
def commit(self):
|
||||
pass
|
||||
|
||||
def refresh(self, item):
|
||||
if not hasattr(item, "status"):
|
||||
item.status = GitStatus.CONNECTED
|
||||
if not hasattr(item, "last_validated"):
|
||||
item.last_validated = "2026-03-08T00:00:00Z"
|
||||
|
||||
def test_get_git_configs_masks_pat():
|
||||
"""
|
||||
@PRE: Database session `db` is available.
|
||||
@POST: Returns a list of all GitServerConfig objects from the database with PAT masked.
|
||||
"""
|
||||
db = DbMock([GitServerConfig(
|
||||
id="config-1", name="Test Server", provider=GitProvider.GITHUB,
|
||||
url="https://github.com", pat="secret-token",
|
||||
status=GitStatus.CONNECTED, last_validated="2026-03-08T00:00:00Z"
|
||||
)])
|
||||
|
||||
result = asyncio.run(git_routes.get_git_configs(db=db))
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].pat == "********"
|
||||
assert result[0].name == "Test Server"
|
||||
|
||||
def test_create_git_config_persists_config():
|
||||
"""
|
||||
@PRE: `config` contains valid GitServerConfigCreate data.
|
||||
@POST: A new GitServerConfig record is created in the database.
|
||||
"""
|
||||
from src.api.routes.git_schemas import GitServerConfigCreate
|
||||
db = DbMock()
|
||||
config = GitServerConfigCreate(
|
||||
name="New Server", provider=GitProvider.GITLAB,
|
||||
url="https://gitlab.com", pat="new-token",
|
||||
default_branch="master"
|
||||
)
|
||||
|
||||
result = asyncio.run(git_routes.create_git_config(config=config, db=db))
|
||||
|
||||
assert len(db._added) == 1
|
||||
assert db._added[0].name == "New Server"
|
||||
assert db._added[0].pat == "new-token"
|
||||
assert result.name == "New Server"
|
||||
assert result.pat == "new-token" # Note: route returns unmasked until serialized by FastAPI usually, but in tests schema might catch it or not.
|
||||
|
||||
from src.api.routes.git_schemas import GitServerConfigUpdate
|
||||
|
||||
def test_update_git_config_modifies_record():
|
||||
"""
|
||||
@PRE: `config_id` corresponds to an existing configuration.
|
||||
@POST: The configuration record is updated in the database, preserving PAT if masked is sent.
|
||||
"""
|
||||
existing_config = GitServerConfig(
|
||||
id="config-1", name="Old Server", provider=GitProvider.GITHUB,
|
||||
url="https://github.com", pat="old-token",
|
||||
status=GitStatus.CONNECTED, last_validated="2026-03-08T00:00:00Z"
|
||||
)
|
||||
# The monkeypatched query will return existing_config as it's the only one in the list
|
||||
class SingleConfigDbMock:
|
||||
def query(self, *args): return self
|
||||
def filter(self, *args): return self
|
||||
def first(self): return existing_config
|
||||
def commit(self): pass
|
||||
def refresh(self, config): pass
|
||||
|
||||
db = SingleConfigDbMock()
|
||||
update_data = GitServerConfigUpdate(name="Updated Server", pat="********")
|
||||
|
||||
result = asyncio.run(git_routes.update_git_config(config_id="config-1", config_update=update_data, db=db))
|
||||
|
||||
assert existing_config.name == "Updated Server"
|
||||
assert existing_config.pat == "old-token" # Ensure PAT is not overwritten with asterisks
|
||||
assert result.pat == "********"
|
||||
|
||||
def test_update_git_config_raises_404_if_not_found():
|
||||
"""
|
||||
@PRE: `config_id` corresponds to a missing configuration.
|
||||
@THROW: HTTPException 404
|
||||
"""
|
||||
db = DbMock([]) # Empty db
|
||||
update_data = GitServerConfigUpdate(name="Updated Server", pat="new-token")
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
asyncio.run(git_routes.update_git_config(config_id="config-1", config_update=update_data, db=db))
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert exc_info.value.detail == "Configuration not found"
|
||||
|
||||
def test_delete_git_config_removes_record():
|
||||
"""
|
||||
@PRE: `config_id` corresponds to an existing configuration.
|
||||
@POST: The configuration record is removed from the database.
|
||||
"""
|
||||
existing_config = GitServerConfig(id="config-1")
|
||||
class SingleConfigDbMock:
|
||||
def query(self, *args): return self
|
||||
def filter(self, *args): return self
|
||||
def first(self): return existing_config
|
||||
def delete(self, config): self.deleted = config
|
||||
def commit(self): pass
|
||||
|
||||
db = SingleConfigDbMock()
|
||||
|
||||
result = asyncio.run(git_routes.delete_git_config(config_id="config-1", db=db))
|
||||
|
||||
assert db.deleted == existing_config
|
||||
assert result["status"] == "success"
|
||||
|
||||
def test_test_git_config_validates_connection_successfully(monkeypatch):
|
||||
"""
|
||||
@PRE: `config` contains provider, url, and pat.
|
||||
@POST: Returns success if the connection is validated via GitService.
|
||||
"""
|
||||
class MockGitService:
|
||||
async def test_connection(self, provider, url, pat):
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", MockGitService())
|
||||
from src.api.routes.git_schemas import GitServerConfigCreate
|
||||
|
||||
config = GitServerConfigCreate(
|
||||
name="Test Server", provider=GitProvider.GITHUB,
|
||||
url="https://github.com", pat="test-pat"
|
||||
)
|
||||
db = DbMock([])
|
||||
|
||||
result = asyncio.run(git_routes.test_git_config(config=config, db=db))
|
||||
|
||||
assert result["status"] == "success"
|
||||
|
||||
def test_test_git_config_fails_validation(monkeypatch):
|
||||
"""
|
||||
@PRE: `config` contains provider, url, and pat BUT connection fails.
|
||||
@THROW: HTTPException 400
|
||||
"""
|
||||
class MockGitService:
|
||||
async def test_connection(self, provider, url, pat):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", MockGitService())
|
||||
from src.api.routes.git_schemas import GitServerConfigCreate
|
||||
|
||||
config = GitServerConfigCreate(
|
||||
name="Test Server", provider=GitProvider.GITHUB,
|
||||
url="https://github.com", pat="bad-pat"
|
||||
)
|
||||
db = DbMock([])
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
asyncio.run(git_routes.test_git_config(config=config, db=db))
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.detail == "Connection failed"
|
||||
|
||||
def test_list_gitea_repositories_returns_payload(monkeypatch):
|
||||
"""
|
||||
@PRE: config_id exists and provider is GITEA.
|
||||
@POST: Returns repositories visible to PAT user.
|
||||
"""
|
||||
class MockGitService:
|
||||
async def list_gitea_repositories(self, url, pat):
|
||||
return [{"name": "test-repo", "full_name": "owner/test-repo", "private": True}]
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", MockGitService())
|
||||
existing_config = GitServerConfig(
|
||||
id="config-1", name="Gitea Server", provider=GitProvider.GITEA,
|
||||
url="https://gitea.local", pat="gitea-token"
|
||||
)
|
||||
db = DbMock([existing_config])
|
||||
|
||||
result = asyncio.run(git_routes.list_gitea_repositories(config_id="config-1", db=db))
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "test-repo"
|
||||
assert result[0].private is True
|
||||
|
||||
def test_list_gitea_repositories_rejects_non_gitea(monkeypatch):
|
||||
"""
|
||||
@PRE: config_id exists and provider is NOT GITEA.
|
||||
@THROW: HTTPException 400
|
||||
"""
|
||||
existing_config = GitServerConfig(
|
||||
id="config-1", name="GitHub Server", provider=GitProvider.GITHUB,
|
||||
url="https://github.com", pat="token"
|
||||
)
|
||||
db = DbMock([existing_config])
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
asyncio.run(git_routes.list_gitea_repositories(config_id="config-1", db=db))
|
||||
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "GITEA provider only" in exc_info.value.detail
|
||||
|
||||
def test_create_remote_repository_creates_provider_repo(monkeypatch):
|
||||
"""
|
||||
@PRE: config_id exists and PAT has creation permissions.
|
||||
@POST: Returns normalized remote repository payload.
|
||||
"""
|
||||
class MockGitService:
|
||||
async def create_gitlab_repository(self, server_url, pat, name, private, description, auto_init, default_branch):
|
||||
return {
|
||||
"name": name,
|
||||
"full_name": f"user/{name}",
|
||||
"private": private,
|
||||
"clone_url": f"{server_url}/user/{name}.git"
|
||||
}
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", MockGitService())
|
||||
from src.api.routes.git_schemas import RemoteRepoCreateRequest
|
||||
|
||||
existing_config = GitServerConfig(
|
||||
id="config-1", name="GitLab Server", provider=GitProvider.GITLAB,
|
||||
url="https://gitlab.com", pat="token"
|
||||
)
|
||||
db = DbMock([existing_config])
|
||||
|
||||
request = RemoteRepoCreateRequest(name="new-repo", private=True, description="desc")
|
||||
result = asyncio.run(git_routes.create_remote_repository(config_id="config-1", request=request, db=db))
|
||||
|
||||
assert result.provider == GitProvider.GITLAB
|
||||
assert result.name == "new-repo"
|
||||
assert result.full_name == "user/new-repo"
|
||||
|
||||
def test_init_repository_initializes_and_saves_binding(monkeypatch):
|
||||
"""
|
||||
@PRE: `dashboard_ref` exists and `init_data` contains valid config_id and remote_url.
|
||||
@POST: Repository is initialized on disk and a GitRepository record is saved in DB.
|
||||
"""
|
||||
from src.api.routes.git_schemas import RepoInitRequest
|
||||
|
||||
class MockGitService:
|
||||
def init_repo(self, dashboard_id, remote_url, pat, repo_key, default_branch):
|
||||
self.init_called = True
|
||||
def _get_repo_path(self, dashboard_id, repo_key):
|
||||
return f"/tmp/repos/{repo_key}"
|
||||
|
||||
git_service_mock = MockGitService()
|
||||
monkeypatch.setattr(git_routes, "git_service", git_service_mock)
|
||||
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *args, **kwargs: 123)
|
||||
monkeypatch.setattr(git_routes, "_resolve_repo_key_from_ref", lambda *args, **kwargs: "dashboard-123")
|
||||
|
||||
existing_config = GitServerConfig(
|
||||
id="config-1", name="GitLab Server", provider=GitProvider.GITLAB,
|
||||
url="https://gitlab.com", pat="token", default_branch="main"
|
||||
)
|
||||
db = DbMock([existing_config])
|
||||
|
||||
init_data = RepoInitRequest(config_id="config-1", remote_url="https://git.local/repo.git")
|
||||
|
||||
result = asyncio.run(git_routes.init_repository(dashboard_ref="123", init_data=init_data, config_manager=MagicMock(), db=db))
|
||||
|
||||
assert result["status"] == "success"
|
||||
assert git_service_mock.init_called is True
|
||||
assert len(db._added) == 1
|
||||
assert isinstance(db._added[0], GitRepository)
|
||||
assert db._added[0].dashboard_id == 123
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_git_api:Module]
|
||||
440
backend/src/api/routes/__tests__/test_git_status_route.py
Normal file
440
backend/src/api/routes/__tests__/test_git_status_route.py
Normal file
@@ -0,0 +1,440 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_git_status_route:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: tests, git, api, status, no_repo
|
||||
# @PURPOSE: Validate status endpoint behavior for missing and error repository states.
|
||||
# @LAYER: Domain (Tests)
|
||||
# @RELATION: VERIFIES -> [backend.src.api.routes.git]
|
||||
|
||||
from fastapi import HTTPException
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from src.api.routes import git as git_routes
|
||||
|
||||
|
||||
# [DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function]
|
||||
# @PURPOSE: Ensure missing local repository is represented as NO_REPO payload instead of an API error.
|
||||
# @PRE: GitService.get_status raises HTTPException(404).
|
||||
# @POST: Route returns a deterministic NO_REPO status payload.
|
||||
def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch):
|
||||
class MissingRepoGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
return f"/tmp/missing-repo-{dashboard_id}"
|
||||
|
||||
def get_status(self, dashboard_id: int) -> dict:
|
||||
raise AssertionError("get_status must not be called when repository path is missing")
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", MissingRepoGitService())
|
||||
|
||||
response = asyncio.run(git_routes.get_repository_status(34))
|
||||
|
||||
assert response["sync_status"] == "NO_REPO"
|
||||
assert response["sync_state"] == "NO_REPO"
|
||||
assert response["has_repo"] is False
|
||||
assert response["current_branch"] is None
|
||||
# [/DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_repository_status_propagates_non_404_http_exception:Function]
|
||||
# @PURPOSE: Ensure HTTP exceptions other than 404 are not masked.
|
||||
# @PRE: GitService.get_status raises HTTPException with non-404 status.
|
||||
# @POST: Raised exception preserves original status and detail.
|
||||
def test_get_repository_status_propagates_non_404_http_exception(monkeypatch):
|
||||
class ConflictGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
return f"/tmp/existing-repo-{dashboard_id}"
|
||||
|
||||
def get_status(self, dashboard_id: int) -> dict:
|
||||
raise HTTPException(status_code=409, detail="Conflict")
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", ConflictGitService())
|
||||
monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
asyncio.run(git_routes.get_repository_status(34))
|
||||
|
||||
assert exc_info.value.status_code == 409
|
||||
assert exc_info.value.detail == "Conflict"
|
||||
# [/DEF:test_get_repository_status_propagates_non_404_http_exception:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_repository_diff_propagates_http_exception:Function]
|
||||
# @PURPOSE: Ensure diff endpoint preserves domain HTTP errors from GitService.
|
||||
# @PRE: GitService.get_diff raises HTTPException.
|
||||
# @POST: Endpoint raises same HTTPException values.
|
||||
def test_get_repository_diff_propagates_http_exception(monkeypatch):
|
||||
class DiffGitService:
|
||||
def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:
|
||||
raise HTTPException(status_code=404, detail="Repository missing")
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", DiffGitService())
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
asyncio.run(git_routes.get_repository_diff(12))
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert exc_info.value.detail == "Repository missing"
|
||||
# [/DEF:test_get_repository_diff_propagates_http_exception:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_history_wraps_unexpected_error_as_500:Function]
|
||||
# @PURPOSE: Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors.
|
||||
# @PRE: GitService.get_commit_history raises ValueError.
|
||||
# @POST: Endpoint returns HTTPException with status 500 and route context.
|
||||
def test_get_history_wraps_unexpected_error_as_500(monkeypatch):
|
||||
class HistoryGitService:
|
||||
def get_commit_history(self, dashboard_id: int, limit: int = 50):
|
||||
raise ValueError("broken parser")
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", HistoryGitService())
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
asyncio.run(git_routes.get_history(12))
|
||||
|
||||
assert exc_info.value.status_code == 500
|
||||
assert exc_info.value.detail == "get_history failed: broken parser"
|
||||
# [/DEF:test_get_history_wraps_unexpected_error_as_500:Function]
|
||||
|
||||
|
||||
# [DEF:test_commit_changes_wraps_unexpected_error_as_500:Function]
|
||||
# @PURPOSE: Ensure commit endpoint does not leak unexpected errors as 400.
|
||||
# @PRE: GitService.commit_changes raises RuntimeError.
|
||||
# @POST: Endpoint raises HTTPException(500) with route context.
|
||||
def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):
|
||||
class CommitGitService:
|
||||
def commit_changes(self, dashboard_id: int, message: str, files):
|
||||
raise RuntimeError("index lock")
|
||||
|
||||
class CommitPayload:
|
||||
message = "test"
|
||||
files = ["dashboards/a.yaml"]
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", CommitGitService())
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
asyncio.run(git_routes.commit_changes(12, CommitPayload()))
|
||||
|
||||
assert exc_info.value.status_code == 500
|
||||
assert exc_info.value.detail == "commit_changes failed: index lock"
|
||||
# [/DEF:test_commit_changes_wraps_unexpected_error_as_500:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_repository_status_batch_returns_mixed_statuses:Function]
|
||||
# @PURPOSE: Ensure batch endpoint returns per-dashboard statuses in one response.
|
||||
# @PRE: Some repositories are missing and some are initialized.
|
||||
# @POST: Returned map includes resolved status for each requested dashboard ID.
|
||||
def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):
|
||||
class BatchGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
return f"/tmp/repo-{dashboard_id}"
|
||||
|
||||
def get_status(self, dashboard_id: int) -> dict:
|
||||
if dashboard_id == 2:
|
||||
return {"sync_state": "SYNCED", "sync_status": "OK"}
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", BatchGitService())
|
||||
monkeypatch.setattr(git_routes.os.path, "exists", lambda path: path.endswith("/repo-2"))
|
||||
|
||||
class BatchRequest:
|
||||
dashboard_ids = [1, 2]
|
||||
|
||||
response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))
|
||||
|
||||
assert response.statuses["1"]["sync_status"] == "NO_REPO"
|
||||
assert response.statuses["2"]["sync_state"] == "SYNCED"
|
||||
# [/DEF:test_get_repository_status_batch_returns_mixed_statuses:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function]
|
||||
# @PURPOSE: Ensure batch endpoint marks failed items as ERROR without failing entire request.
|
||||
# @PRE: GitService raises non-HTTP exception for one dashboard.
|
||||
# @POST: Failed dashboard status is marked as ERROR.
|
||||
def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch):
|
||||
class BatchErrorGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
return f"/tmp/repo-{dashboard_id}"
|
||||
|
||||
def get_status(self, dashboard_id: int) -> dict:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", BatchErrorGitService())
|
||||
monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True)
|
||||
|
||||
class BatchRequest:
|
||||
dashboard_ids = [9]
|
||||
|
||||
response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))
|
||||
|
||||
assert response.statuses["9"]["sync_status"] == "ERROR"
|
||||
assert response.statuses["9"]["sync_state"] == "ERROR"
|
||||
# [/DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function]
|
||||
# @PURPOSE: Ensure batch endpoint protects server from oversized payloads.
|
||||
# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.
|
||||
# @POST: Result contains unique IDs up to configured cap.
|
||||
def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch):
|
||||
class SafeBatchGitService:
|
||||
def _get_repo_path(self, dashboard_id: int) -> str:
|
||||
return f"/tmp/repo-{dashboard_id}"
|
||||
|
||||
def get_status(self, dashboard_id: int) -> dict:
|
||||
return {"sync_state": "SYNCED", "sync_status": "OK"}
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", SafeBatchGitService())
|
||||
monkeypatch.setattr(git_routes.os.path, "exists", lambda _path: True)
|
||||
|
||||
class BatchRequest:
|
||||
dashboard_ids = [1, 1] + list(range(2, 90))
|
||||
|
||||
response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))
|
||||
|
||||
assert len(response.statuses) == git_routes.MAX_REPOSITORY_STATUS_BATCH
|
||||
assert "1" in response.statuses
|
||||
# [/DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function]
|
||||
|
||||
|
||||
# [DEF:test_commit_changes_applies_profile_identity_before_commit:Function]
|
||||
# @PURPOSE: Ensure commit route configures repository identity from profile preferences before commit call.
|
||||
# @PRE: Profile preference contains git_username/git_email for current user.
|
||||
# @POST: git_service.configure_identity receives resolved identity and commit proceeds.
|
||||
def test_commit_changes_applies_profile_identity_before_commit(monkeypatch):
|
||||
class IdentityGitService:
|
||||
def __init__(self):
|
||||
self.configured_identity = None
|
||||
self.commit_payload = None
|
||||
|
||||
def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):
|
||||
self.configured_identity = (dashboard_id, git_username, git_email)
|
||||
|
||||
def commit_changes(self, dashboard_id: int, message: str, files):
|
||||
self.commit_payload = (dashboard_id, message, files)
|
||||
|
||||
class PreferenceRow:
|
||||
git_username = "user_1"
|
||||
git_email = "user1@mail.ru"
|
||||
|
||||
class PreferenceQuery:
|
||||
def filter(self, *_args, **_kwargs):
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
return PreferenceRow()
|
||||
|
||||
class DbStub:
|
||||
def query(self, _model):
|
||||
return PreferenceQuery()
|
||||
|
||||
class UserStub:
|
||||
id = "u-1"
|
||||
|
||||
class CommitPayload:
|
||||
message = "test"
|
||||
files = ["dashboards/a.yaml"]
|
||||
|
||||
identity_service = IdentityGitService()
|
||||
monkeypatch.setattr(git_routes, "git_service", identity_service)
|
||||
monkeypatch.setattr(
|
||||
git_routes,
|
||||
"_resolve_dashboard_id_from_ref",
|
||||
lambda *_args, **_kwargs: 12,
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
git_routes.commit_changes(
|
||||
"dashboard-12",
|
||||
CommitPayload(),
|
||||
config_manager=MagicMock(),
|
||||
db=DbStub(),
|
||||
current_user=UserStub(),
|
||||
)
|
||||
)
|
||||
|
||||
assert identity_service.configured_identity == (12, "user_1", "user1@mail.ru")
|
||||
assert identity_service.commit_payload == (12, "test", ["dashboards/a.yaml"])
|
||||
# [/DEF:test_commit_changes_applies_profile_identity_before_commit:Function]
|
||||
|
||||
|
||||
# [DEF:test_pull_changes_applies_profile_identity_before_pull:Function]
|
||||
# @PURPOSE: Ensure pull route configures repository identity from profile preferences before pull call.
|
||||
# @PRE: Profile preference contains git_username/git_email for current user.
|
||||
# @POST: git_service.configure_identity receives resolved identity and pull proceeds.
|
||||
def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
|
||||
class IdentityGitService:
|
||||
def __init__(self):
|
||||
self.configured_identity = None
|
||||
self.pulled_dashboard_id = None
|
||||
|
||||
def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):
|
||||
self.configured_identity = (dashboard_id, git_username, git_email)
|
||||
|
||||
def pull_changes(self, dashboard_id: int):
|
||||
self.pulled_dashboard_id = dashboard_id
|
||||
|
||||
class PreferenceRow:
|
||||
git_username = "user_1"
|
||||
git_email = "user1@mail.ru"
|
||||
|
||||
class PreferenceQuery:
|
||||
def filter(self, *_args, **_kwargs):
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
return PreferenceRow()
|
||||
|
||||
class DbStub:
|
||||
def query(self, _model):
|
||||
return PreferenceQuery()
|
||||
|
||||
class UserStub:
|
||||
id = "u-1"
|
||||
|
||||
identity_service = IdentityGitService()
|
||||
monkeypatch.setattr(git_routes, "git_service", identity_service)
|
||||
monkeypatch.setattr(
|
||||
git_routes,
|
||||
"_resolve_dashboard_id_from_ref",
|
||||
lambda *_args, **_kwargs: 12,
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
git_routes.pull_changes(
|
||||
"dashboard-12",
|
||||
config_manager=MagicMock(),
|
||||
db=DbStub(),
|
||||
current_user=UserStub(),
|
||||
)
|
||||
)
|
||||
|
||||
assert identity_service.configured_identity == (12, "user_1", "user1@mail.ru")
|
||||
assert identity_service.pulled_dashboard_id == 12
|
||||
# [/DEF:test_pull_changes_applies_profile_identity_before_pull:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_merge_status_returns_service_payload:Function]
|
||||
# @PURPOSE: Ensure merge status route returns service payload as-is.
|
||||
# @PRE: git_service.get_merge_status returns unfinished merge payload.
|
||||
# @POST: Route response contains has_unfinished_merge=True.
|
||||
def test_get_merge_status_returns_service_payload(monkeypatch):
|
||||
class MergeStatusGitService:
|
||||
def get_merge_status(self, dashboard_id: int) -> dict:
|
||||
return {
|
||||
"has_unfinished_merge": True,
|
||||
"repository_path": "/tmp/repo-12",
|
||||
"git_dir": "/tmp/repo-12/.git",
|
||||
"current_branch": "dev",
|
||||
"merge_head": "abc",
|
||||
"merge_message_preview": "merge msg",
|
||||
"conflicts_count": 2,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", MergeStatusGitService())
|
||||
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *_args, **_kwargs: 12)
|
||||
|
||||
response = asyncio.run(
|
||||
git_routes.get_merge_status(
|
||||
"dashboard-12",
|
||||
config_manager=MagicMock(),
|
||||
)
|
||||
)
|
||||
|
||||
assert response["has_unfinished_merge"] is True
|
||||
assert response["conflicts_count"] == 2
|
||||
# [/DEF:test_get_merge_status_returns_service_payload:Function]
|
||||
|
||||
|
||||
# [DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function]
|
||||
# @PURPOSE: Ensure merge resolve route forwards parsed resolutions to service.
|
||||
# @PRE: resolve_data has one file strategy.
|
||||
# @POST: Service receives normalized list and route returns resolved files.
|
||||
def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
class MergeResolveGitService:
|
||||
def resolve_merge_conflicts(self, dashboard_id: int, resolutions):
|
||||
captured["dashboard_id"] = dashboard_id
|
||||
captured["resolutions"] = resolutions
|
||||
return ["dashboards/a.yaml"]
|
||||
|
||||
class ResolveData:
|
||||
class _Resolution:
|
||||
def dict(self):
|
||||
return {"file_path": "dashboards/a.yaml", "resolution": "mine", "content": None}
|
||||
|
||||
resolutions = [_Resolution()]
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", MergeResolveGitService())
|
||||
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *_args, **_kwargs: 12)
|
||||
|
||||
response = asyncio.run(
|
||||
git_routes.resolve_merge_conflicts(
|
||||
"dashboard-12",
|
||||
ResolveData(),
|
||||
config_manager=MagicMock(),
|
||||
)
|
||||
)
|
||||
|
||||
assert captured["dashboard_id"] == 12
|
||||
assert captured["resolutions"][0]["resolution"] == "mine"
|
||||
assert response["resolved_files"] == ["dashboards/a.yaml"]
|
||||
# [/DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function]
|
||||
|
||||
|
||||
# [DEF:test_abort_merge_calls_service_and_returns_result:Function]
|
||||
# @PURPOSE: Ensure abort route delegates to service.
|
||||
# @PRE: Service abort_merge returns aborted status.
|
||||
# @POST: Route returns aborted status.
|
||||
def test_abort_merge_calls_service_and_returns_result(monkeypatch):
|
||||
class AbortGitService:
|
||||
def abort_merge(self, dashboard_id: int):
|
||||
assert dashboard_id == 12
|
||||
return {"status": "aborted"}
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", AbortGitService())
|
||||
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *_args, **_kwargs: 12)
|
||||
|
||||
response = asyncio.run(
|
||||
git_routes.abort_merge(
|
||||
"dashboard-12",
|
||||
config_manager=MagicMock(),
|
||||
)
|
||||
)
|
||||
|
||||
assert response["status"] == "aborted"
|
||||
# [/DEF:test_abort_merge_calls_service_and_returns_result:Function]
|
||||
|
||||
|
||||
# [DEF:test_continue_merge_passes_message_and_returns_commit:Function]
|
||||
# @PURPOSE: Ensure continue route passes commit message to service.
|
||||
# @PRE: continue_data.message is provided.
|
||||
# @POST: Route returns committed status and hash.
|
||||
def test_continue_merge_passes_message_and_returns_commit(monkeypatch):
|
||||
class ContinueGitService:
|
||||
def continue_merge(self, dashboard_id: int, message: str):
|
||||
assert dashboard_id == 12
|
||||
assert message == "Resolve all conflicts"
|
||||
return {"status": "committed", "commit_hash": "abc123"}
|
||||
|
||||
class ContinueData:
|
||||
message = "Resolve all conflicts"
|
||||
|
||||
monkeypatch.setattr(git_routes, "git_service", ContinueGitService())
|
||||
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *_args, **_kwargs: 12)
|
||||
|
||||
response = asyncio.run(
|
||||
git_routes.continue_merge(
|
||||
"dashboard-12",
|
||||
ContinueData(),
|
||||
config_manager=MagicMock(),
|
||||
)
|
||||
)
|
||||
|
||||
assert response["status"] == "committed"
|
||||
assert response["commit_hash"] == "abc123"
|
||||
# [/DEF:test_continue_merge_passes_message_and_returns_commit:Function]
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_git_status_route:Module]
|
||||
510
backend/src/api/routes/__tests__/test_migration_routes.py
Normal file
510
backend/src/api/routes/__tests__/test_migration_routes.py
Normal file
@@ -0,0 +1,510 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_migration_routes:Module]
|
||||
#
|
||||
# @COMPLEXITY: 3
|
||||
# @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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dry_run_migration_returns_diff_and_risk(db_session):
|
||||
# @TEST_EDGE: missing_target_datasource -> validates high risk item generation
|
||||
# @TEST_EDGE: breaking_reference -> validates high risk on missing dataset link
|
||||
from src.api.routes.migration import dry_run_migration
|
||||
from src.models.dashboard import DashboardSelection
|
||||
|
||||
env_source = MagicMock()
|
||||
env_source.id = "src"
|
||||
env_source.name = "Source"
|
||||
env_source.url = "http://source"
|
||||
env_source.username = "admin"
|
||||
env_source.password = "admin"
|
||||
env_source.verify_ssl = False
|
||||
env_source.timeout = 30
|
||||
|
||||
env_target = MagicMock()
|
||||
env_target.id = "tgt"
|
||||
env_target.name = "Target"
|
||||
env_target.url = "http://target"
|
||||
env_target.username = "admin"
|
||||
env_target.password = "admin"
|
||||
env_target.verify_ssl = False
|
||||
env_target.timeout = 30
|
||||
|
||||
cm = _make_sync_config_manager([env_source, env_target])
|
||||
selection = DashboardSelection(
|
||||
selected_ids=[42],
|
||||
source_env_id="src",
|
||||
target_env_id="tgt",
|
||||
replace_db_config=False,
|
||||
fix_cross_filters=True,
|
||||
)
|
||||
|
||||
with patch("src.api.routes.migration.SupersetClient") as MockClient, \
|
||||
patch("src.api.routes.migration.MigrationDryRunService") as MockService:
|
||||
source_client = MagicMock()
|
||||
target_client = MagicMock()
|
||||
MockClient.side_effect = [source_client, target_client]
|
||||
|
||||
service_instance = MagicMock()
|
||||
service_payload = {
|
||||
"generated_at": "2026-02-27T00:00:00+00:00",
|
||||
"selection": selection.model_dump(),
|
||||
"selected_dashboard_titles": ["Sales"],
|
||||
"diff": {
|
||||
"dashboards": {"create": [], "update": [{"uuid": "dash-1"}], "delete": []},
|
||||
"charts": {"create": [{"uuid": "chart-1"}], "update": [], "delete": []},
|
||||
"datasets": {"create": [{"uuid": "dataset-1"}], "update": [], "delete": []},
|
||||
},
|
||||
"summary": {
|
||||
"dashboards": {"create": 0, "update": 1, "delete": 0},
|
||||
"charts": {"create": 1, "update": 0, "delete": 0},
|
||||
"datasets": {"create": 1, "update": 0, "delete": 0},
|
||||
"selected_dashboards": 1,
|
||||
},
|
||||
"risk": {
|
||||
"score": 75,
|
||||
"level": "high",
|
||||
"items": [
|
||||
{"code": "missing_datasource"},
|
||||
{"code": "breaking_reference"},
|
||||
],
|
||||
},
|
||||
}
|
||||
service_instance.run.return_value = service_payload
|
||||
MockService.return_value = service_instance
|
||||
|
||||
result = await dry_run_migration(selection=selection, config_manager=cm, db=db_session, _=None)
|
||||
|
||||
assert result["summary"]["dashboards"]["update"] == 1
|
||||
assert result["summary"]["charts"]["create"] == 1
|
||||
assert result["summary"]["datasets"]["create"] == 1
|
||||
assert result["risk"]["score"] > 0
|
||||
assert any(item["code"] == "missing_datasource" for item in result["risk"]["items"])
|
||||
assert any(item["code"] == "breaking_reference" for item in result["risk"]["items"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dry_run_migration_rejects_same_environment(db_session):
|
||||
from src.api.routes.migration import dry_run_migration
|
||||
from src.models.dashboard import DashboardSelection
|
||||
|
||||
env = MagicMock()
|
||||
env.id = "same"
|
||||
env.name = "Same"
|
||||
env.url = "http://same"
|
||||
env.username = "admin"
|
||||
env.password = "admin"
|
||||
env.verify_ssl = False
|
||||
env.timeout = 30
|
||||
|
||||
cm = _make_sync_config_manager([env])
|
||||
selection = DashboardSelection(selected_ids=[1], source_env_id="same", target_env_id="same")
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await dry_run_migration(selection=selection, config_manager=cm, db=db_session, _=None)
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_migration_routes:Module]
|
||||
298
backend/src/api/routes/__tests__/test_profile_api.py
Normal file
298
backend/src/api/routes/__tests__/test_profile_api.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# [DEF:backend.src.api.routes.__tests__.test_profile_api:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: tests, profile, api, preferences, lookup, contract
|
||||
# @PURPOSE: Verifies profile API route contracts for preference read/update and Superset account lookup.
|
||||
# @LAYER: API
|
||||
# @RELATION: TESTS -> backend.src.api.routes.profile
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.app import app
|
||||
from src.core.database import get_db
|
||||
from src.dependencies import get_config_manager, get_current_user
|
||||
from src.schemas.profile import (
|
||||
ProfilePermissionState,
|
||||
ProfilePreference,
|
||||
ProfilePreferenceResponse,
|
||||
ProfileSecuritySummary,
|
||||
SupersetAccountCandidate,
|
||||
SupersetAccountLookupResponse,
|
||||
)
|
||||
from src.services.profile_service import (
|
||||
EnvironmentNotFoundError,
|
||||
ProfileAuthorizationError,
|
||||
ProfileValidationError,
|
||||
)
|
||||
# [/SECTION]
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
# [DEF:mock_profile_route_dependencies:Function]
|
||||
# @PURPOSE: Provides deterministic dependency overrides for profile route tests.
|
||||
# @PRE: App instance is initialized.
|
||||
# @POST: Dependencies are overridden for current test and restored afterward.
|
||||
def mock_profile_route_dependencies():
|
||||
mock_user = MagicMock()
|
||||
mock_user.id = "u-1"
|
||||
mock_user.username = "test-user"
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_config_manager = MagicMock()
|
||||
|
||||
app.dependency_overrides[get_current_user] = lambda: mock_user
|
||||
app.dependency_overrides[get_db] = lambda: mock_db
|
||||
app.dependency_overrides[get_config_manager] = lambda: mock_config_manager
|
||||
|
||||
return mock_user, mock_db, mock_config_manager
|
||||
# [/DEF:mock_profile_route_dependencies:Function]
|
||||
|
||||
|
||||
# [DEF:profile_route_deps_fixture:Function]
|
||||
# @PURPOSE: Pytest fixture wrapper for profile route dependency overrides.
|
||||
# @PRE: None.
|
||||
# @POST: Yields overridden dependencies and clears overrides after test.
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def profile_route_deps_fixture():
|
||||
yielded = mock_profile_route_dependencies()
|
||||
yield yielded
|
||||
app.dependency_overrides.clear()
|
||||
# [/DEF:profile_route_deps_fixture:Function]
|
||||
|
||||
|
||||
# [DEF:_build_preference_response:Function]
|
||||
# @PURPOSE: Builds stable profile preference response payload for route tests.
|
||||
# @PRE: user_id is provided.
|
||||
# @POST: Returns ProfilePreferenceResponse object with deterministic timestamps.
|
||||
def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceResponse:
|
||||
now = datetime.now(timezone.utc)
|
||||
return ProfilePreferenceResponse(
|
||||
status="success",
|
||||
message="Preference loaded",
|
||||
preference=ProfilePreference(
|
||||
user_id=user_id,
|
||||
superset_username="John_Doe",
|
||||
superset_username_normalized="john_doe",
|
||||
show_only_my_dashboards=True,
|
||||
show_only_slug_dashboards=True,
|
||||
git_username="ivan.ivanov",
|
||||
git_email="ivan@company.local",
|
||||
has_git_personal_access_token=True,
|
||||
git_personal_access_token_masked="iv***al",
|
||||
start_page="reports",
|
||||
auto_open_task_drawer=False,
|
||||
dashboards_table_density="compact",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
security=ProfileSecuritySummary(
|
||||
read_only=True,
|
||||
auth_source="adfs",
|
||||
current_role="Data Engineer",
|
||||
role_source="adfs",
|
||||
roles=["Data Engineer"],
|
||||
permissions=[
|
||||
ProfilePermissionState(key="migration:run", allowed=True),
|
||||
ProfilePermissionState(key="admin:users", allowed=False),
|
||||
],
|
||||
),
|
||||
)
|
||||
# [/DEF:_build_preference_response:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_profile_preferences_returns_self_payload:Function]
|
||||
# @PURPOSE: Verifies GET /api/profile/preferences returns stable self-scoped payload.
|
||||
# @PRE: Authenticated user context is available.
|
||||
# @POST: Response status is 200 and payload contains current user preference.
|
||||
def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture):
|
||||
mock_user, _, _ = profile_route_deps_fixture
|
||||
service = MagicMock()
|
||||
service.get_my_preference.return_value = _build_preference_response(user_id=mock_user.id)
|
||||
|
||||
with patch("src.api.routes.profile._get_profile_service", return_value=service):
|
||||
response = client.get("/api/profile/preferences")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["status"] == "success"
|
||||
assert payload["preference"]["user_id"] == mock_user.id
|
||||
assert payload["preference"]["superset_username_normalized"] == "john_doe"
|
||||
assert payload["preference"]["git_username"] == "ivan.ivanov"
|
||||
assert payload["preference"]["git_email"] == "ivan@company.local"
|
||||
assert payload["preference"]["show_only_slug_dashboards"] is True
|
||||
assert payload["preference"]["has_git_personal_access_token"] is True
|
||||
assert payload["preference"]["git_personal_access_token_masked"] == "iv***al"
|
||||
assert payload["preference"]["start_page"] == "reports"
|
||||
assert payload["preference"]["auto_open_task_drawer"] is False
|
||||
assert payload["preference"]["dashboards_table_density"] == "compact"
|
||||
assert payload["security"]["read_only"] is True
|
||||
assert payload["security"]["current_role"] == "Data Engineer"
|
||||
assert payload["security"]["permissions"][0]["key"] == "migration:run"
|
||||
service.get_my_preference.assert_called_once_with(mock_user)
|
||||
# [/DEF:test_get_profile_preferences_returns_self_payload:Function]
|
||||
|
||||
|
||||
# [DEF:test_patch_profile_preferences_success:Function]
|
||||
# @PURPOSE: Verifies PATCH /api/profile/preferences persists valid payload through route mapping.
|
||||
# @PRE: Valid request payload and authenticated user.
|
||||
# @POST: Response status is 200 with saved preference payload.
|
||||
def test_patch_profile_preferences_success(profile_route_deps_fixture):
|
||||
mock_user, _, _ = profile_route_deps_fixture
|
||||
service = MagicMock()
|
||||
service.update_my_preference.return_value = _build_preference_response(user_id=mock_user.id)
|
||||
|
||||
with patch("src.api.routes.profile._get_profile_service", return_value=service):
|
||||
response = client.patch(
|
||||
"/api/profile/preferences",
|
||||
json={
|
||||
"superset_username": "John_Doe",
|
||||
"show_only_my_dashboards": True,
|
||||
"show_only_slug_dashboards": True,
|
||||
"git_username": "ivan.ivanov",
|
||||
"git_email": "ivan@company.local",
|
||||
"git_personal_access_token": "ghp_1234567890",
|
||||
"start_page": "reports-logs",
|
||||
"auto_open_task_drawer": False,
|
||||
"dashboards_table_density": "free",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["status"] == "success"
|
||||
assert payload["preference"]["superset_username"] == "John_Doe"
|
||||
assert payload["preference"]["show_only_my_dashboards"] is True
|
||||
assert payload["preference"]["show_only_slug_dashboards"] is True
|
||||
assert payload["preference"]["git_username"] == "ivan.ivanov"
|
||||
assert payload["preference"]["git_email"] == "ivan@company.local"
|
||||
assert payload["preference"]["start_page"] == "reports"
|
||||
assert payload["preference"]["auto_open_task_drawer"] is False
|
||||
assert payload["preference"]["dashboards_table_density"] == "compact"
|
||||
service.update_my_preference.assert_called_once()
|
||||
|
||||
called_kwargs = service.update_my_preference.call_args.kwargs
|
||||
assert called_kwargs["current_user"] == mock_user
|
||||
assert called_kwargs["payload"].git_username == "ivan.ivanov"
|
||||
assert called_kwargs["payload"].git_email == "ivan@company.local"
|
||||
assert called_kwargs["payload"].git_personal_access_token == "ghp_1234567890"
|
||||
assert called_kwargs["payload"].show_only_slug_dashboards is True
|
||||
assert called_kwargs["payload"].start_page == "reports-logs"
|
||||
assert called_kwargs["payload"].auto_open_task_drawer is False
|
||||
assert called_kwargs["payload"].dashboards_table_density == "free"
|
||||
# [/DEF:test_patch_profile_preferences_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_patch_profile_preferences_validation_error:Function]
|
||||
# @PURPOSE: Verifies route maps domain validation failure to HTTP 422 with actionable details.
|
||||
# @PRE: Service raises ProfileValidationError.
|
||||
# @POST: Response status is 422 and includes validation messages.
|
||||
def test_patch_profile_preferences_validation_error(profile_route_deps_fixture):
|
||||
service = MagicMock()
|
||||
service.update_my_preference.side_effect = ProfileValidationError(
|
||||
["Superset username is required when default filter is enabled."]
|
||||
)
|
||||
|
||||
with patch("src.api.routes.profile._get_profile_service", return_value=service):
|
||||
response = client.patch(
|
||||
"/api/profile/preferences",
|
||||
json={
|
||||
"superset_username": "",
|
||||
"show_only_my_dashboards": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
payload = response.json()
|
||||
assert "detail" in payload
|
||||
assert "Superset username is required when default filter is enabled." in payload["detail"]
|
||||
# [/DEF:test_patch_profile_preferences_validation_error:Function]
|
||||
|
||||
|
||||
# [DEF:test_patch_profile_preferences_cross_user_denied:Function]
|
||||
# @PURPOSE: Verifies route maps domain authorization guard failure to HTTP 403.
|
||||
# @PRE: Service raises ProfileAuthorizationError.
|
||||
# @POST: Response status is 403 with denial message.
|
||||
def test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture):
|
||||
service = MagicMock()
|
||||
service.update_my_preference.side_effect = ProfileAuthorizationError(
|
||||
"Cross-user preference mutation is forbidden"
|
||||
)
|
||||
|
||||
with patch("src.api.routes.profile._get_profile_service", return_value=service):
|
||||
response = client.patch(
|
||||
"/api/profile/preferences",
|
||||
json={
|
||||
"superset_username": "john_doe",
|
||||
"show_only_my_dashboards": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
payload = response.json()
|
||||
assert payload["detail"] == "Cross-user preference mutation is forbidden"
|
||||
# [/DEF:test_patch_profile_preferences_cross_user_denied:Function]
|
||||
|
||||
|
||||
# [DEF:test_lookup_superset_accounts_success:Function]
|
||||
# @PURPOSE: Verifies lookup route returns success payload with normalized candidates.
|
||||
# @PRE: Valid environment_id and service success response.
|
||||
# @POST: Response status is 200 and items list is returned.
|
||||
def test_lookup_superset_accounts_success(profile_route_deps_fixture):
|
||||
service = MagicMock()
|
||||
service.lookup_superset_accounts.return_value = SupersetAccountLookupResponse(
|
||||
status="success",
|
||||
environment_id="dev",
|
||||
page_index=0,
|
||||
page_size=20,
|
||||
total=1,
|
||||
warning=None,
|
||||
items=[
|
||||
SupersetAccountCandidate(
|
||||
environment_id="dev",
|
||||
username="john_doe",
|
||||
display_name="John Doe",
|
||||
email="john@example.local",
|
||||
is_active=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
with patch("src.api.routes.profile._get_profile_service", return_value=service):
|
||||
response = client.get("/api/profile/superset-accounts?environment_id=dev")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["status"] == "success"
|
||||
assert payload["environment_id"] == "dev"
|
||||
assert payload["total"] == 1
|
||||
assert payload["items"][0]["username"] == "john_doe"
|
||||
# [/DEF:test_lookup_superset_accounts_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_lookup_superset_accounts_env_not_found:Function]
|
||||
# @PURPOSE: Verifies lookup route maps missing environment to HTTP 404.
|
||||
# @PRE: Service raises EnvironmentNotFoundError.
|
||||
# @POST: Response status is 404 with explicit message.
|
||||
def test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):
|
||||
service = MagicMock()
|
||||
service.lookup_superset_accounts.side_effect = EnvironmentNotFoundError(
|
||||
"Environment 'missing-env' not found"
|
||||
)
|
||||
|
||||
with patch("src.api.routes.profile._get_profile_service", return_value=service):
|
||||
response = client.get("/api/profile/superset-accounts?environment_id=missing-env")
|
||||
|
||||
assert response.status_code == 404
|
||||
payload = response.json()
|
||||
assert payload["detail"] == "Environment 'missing-env' not found"
|
||||
# [/DEF:test_lookup_superset_accounts_env_not_found:Function]
|
||||
|
||||
# [/DEF:backend.src.api.routes.__tests__.test_profile_api:Module]
|
||||
@@ -1,5 +1,5 @@
|
||||
# [DEF:backend.tests.test_reports_api:Module]
|
||||
# @TIER: CRITICAL
|
||||
# @COMPLEXITY: 3
|
||||
# @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
|
||||
# @COMPLEXITY: 3
|
||||
# @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
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: tests, reports, openapi, conformance
|
||||
# @PURPOSE: Validate implemented reports payload shape against OpenAPI-required top-level contract fields.
|
||||
# @LAYER: Domain (Tests)
|
||||
|
||||
73
backend/src/api/routes/__tests__/test_tasks_logs.py
Normal file
73
backend/src/api/routes/__tests__/test_tasks_logs.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# [DEF:__tests__/test_tasks_logs:Module]
|
||||
# @RELATION: VERIFIES -> ../tasks.py
|
||||
# @PURPOSE: Contract testing for task logs API endpoints.
|
||||
# [/DEF:__tests__/test_tasks_logs:Module]
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import MagicMock
|
||||
from src.dependencies import get_task_manager, has_permission
|
||||
from src.api.routes.tasks import router
|
||||
|
||||
# @TEST_FIXTURE: mock_app
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/tasks")
|
||||
|
||||
# Mock TaskManager
|
||||
mock_tm = MagicMock()
|
||||
app.dependency_overrides[get_task_manager] = lambda: mock_tm
|
||||
|
||||
# Mock permissions (bypass for unit test)
|
||||
app.dependency_overrides[has_permission("tasks", "READ")] = lambda: True
|
||||
|
||||
return TestClient(app), mock_tm
|
||||
|
||||
# @TEST_CONTRACT: get_task_logs_api -> Invariants
|
||||
# @TEST_FIXTURE: valid_task_logs_request
|
||||
def test_get_task_logs_success(client):
|
||||
tc, tm = client
|
||||
|
||||
# Setup mock task
|
||||
mock_task = MagicMock()
|
||||
tm.get_task.return_value = mock_task
|
||||
tm.get_task_logs.return_value = [{"level": "INFO", "message": "msg1"}]
|
||||
|
||||
response = tc.get("/tasks/task-1/logs?level=INFO")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"level": "INFO", "message": "msg1"}]
|
||||
tm.get_task.assert_called_with("task-1")
|
||||
# Verify filter construction inside route
|
||||
args = tm.get_task_logs.call_args
|
||||
assert args[0][0] == "task-1"
|
||||
assert args[0][1].level == "INFO"
|
||||
|
||||
# @TEST_EDGE: task_not_found
|
||||
def test_get_task_logs_not_found(client):
|
||||
tc, tm = client
|
||||
tm.get_task.return_value = None
|
||||
|
||||
response = tc.get("/tasks/missing/logs")
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "Task not found"
|
||||
|
||||
# @TEST_EDGE: invalid_limit
|
||||
def test_get_task_logs_invalid_limit(client):
|
||||
tc, tm = client
|
||||
# limit=0 is ge=1 in Query
|
||||
response = tc.get("/tasks/task-1/logs?limit=0")
|
||||
assert response.status_code == 422
|
||||
|
||||
# @TEST_INVARIANT: response_purity
|
||||
def test_get_task_log_stats_success(client):
|
||||
tc, tm = client
|
||||
tm.get_task.return_value = MagicMock()
|
||||
tm.get_task_log_stats.return_value = {"INFO": 5, "ERROR": 1}
|
||||
|
||||
response = tc.get("/tasks/task-1/logs/stats")
|
||||
assert response.status_code == 200
|
||||
# response_model=LogStats might wrap this, but let's check basic structure
|
||||
# assuming tm.get_task_log_stats returns something compatible with LogStats
|
||||
@@ -1,11 +1,11 @@
|
||||
# [DEF:backend.src.api.routes.admin:Module]
|
||||
# [DEF:AdminApi:Module]
|
||||
#
|
||||
# @TIER: STANDARD
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: api, admin, users, roles, permissions
|
||||
# @PURPOSE: Admin API endpoints for user and role management.
|
||||
# @LAYER: API
|
||||
# @RELATION: USES -> backend.src.core.auth.repository.AuthRepository
|
||||
# @RELATION: USES -> backend.src.dependencies.has_permission
|
||||
# @RELATION: [USES] ->[backend.src.core.auth.repository.AuthRepository]
|
||||
# @RELATION: [USES] ->[backend.src.dependencies.has_permission]
|
||||
#
|
||||
# @INVARIANT: All endpoints in this module require 'Admin' role or 'admin' scope.
|
||||
|
||||
@@ -22,8 +22,12 @@ from ...schemas.auth import (
|
||||
ADGroupMappingSchema, ADGroupMappingCreate
|
||||
)
|
||||
from ...models.auth import User, Role, ADGroupMapping
|
||||
from ...dependencies import has_permission
|
||||
from ...dependencies import has_permission, get_plugin_loader
|
||||
from ...core.logger import logger, belief_scope
|
||||
from ...services.rbac_permission_catalog import (
|
||||
discover_declared_permissions,
|
||||
sync_permission_catalog,
|
||||
)
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:router:Variable]
|
||||
@@ -32,6 +36,7 @@ router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
# [/DEF:router:Variable]
|
||||
|
||||
# [DEF:list_users:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Lists all registered users.
|
||||
# @PRE: Current user has 'Admin' role.
|
||||
# @POST: Returns a list of UserSchema objects.
|
||||
@@ -48,6 +53,7 @@ async def list_users(
|
||||
# [/DEF:list_users:Function]
|
||||
|
||||
# [DEF:create_user:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Creates a new local user.
|
||||
# @PRE: Current user has 'Admin' role.
|
||||
# @POST: New user is created in the database.
|
||||
@@ -85,7 +91,14 @@ async def create_user(
|
||||
# [/DEF:create_user:Function]
|
||||
|
||||
# [DEF:update_user:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Updates an existing user.
|
||||
# @PRE: Current user has 'Admin' role.
|
||||
# @POST: User record is updated in the database.
|
||||
# @PARAM: user_id (str) - Target user UUID.
|
||||
# @PARAM: user_in (UserUpdate) - Updated user data.
|
||||
# @PARAM: db (Session) - Auth database session.
|
||||
# @RETURN: UserSchema - The updated user profile.
|
||||
@router.put("/users/{user_id}", response_model=UserSchema)
|
||||
async def update_user(
|
||||
user_id: str,
|
||||
@@ -119,7 +132,13 @@ async def update_user(
|
||||
# [/DEF:update_user:Function]
|
||||
|
||||
# [DEF:delete_user:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Deletes a user.
|
||||
# @PRE: Current user has 'Admin' role.
|
||||
# @POST: User record is removed from the database.
|
||||
# @PARAM: user_id (str) - Target user UUID.
|
||||
# @PARAM: db (Session) - Auth database session.
|
||||
# @RETURN: None
|
||||
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
user_id: str,
|
||||
@@ -142,6 +161,7 @@ async def delete_user(
|
||||
# [/DEF:delete_user:Function]
|
||||
|
||||
# [DEF:list_roles:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Lists all available roles.
|
||||
# @RETURN: List[RoleSchema] - List of roles.
|
||||
# @RELATION: CALLS -> backend.src.models.auth.Role
|
||||
@@ -155,6 +175,7 @@ async def list_roles(
|
||||
# [/DEF:list_roles:Function]
|
||||
|
||||
# [DEF:create_role:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Creates a new system role with associated permissions.
|
||||
# @PRE: Role name must be unique.
|
||||
# @POST: New Role record is created in auth.db.
|
||||
@@ -192,6 +213,7 @@ async def create_role(
|
||||
# [/DEF:create_role:Function]
|
||||
|
||||
# [DEF:update_role:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Updates an existing role's metadata and permissions.
|
||||
# @PRE: role_id must be a valid existing role UUID.
|
||||
# @POST: Role record is updated in auth.db.
|
||||
@@ -236,6 +258,7 @@ async def update_role(
|
||||
# [/DEF:update_role:Function]
|
||||
|
||||
# [DEF:delete_role:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Removes a role from the system.
|
||||
# @PRE: role_id must be a valid existing role UUID.
|
||||
# @POST: Role record is removed from auth.db.
|
||||
@@ -262,6 +285,7 @@ async def delete_role(
|
||||
# [/DEF:delete_role:Function]
|
||||
|
||||
# [DEF:list_permissions:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Lists all available system permissions for assignment.
|
||||
# @POST: Returns a list of all PermissionSchema objects.
|
||||
# @PARAM: db (Session) - Auth database session.
|
||||
@@ -270,14 +294,24 @@ async def delete_role(
|
||||
@router.get("/permissions", response_model=List[PermissionSchema])
|
||||
async def list_permissions(
|
||||
db: Session = Depends(get_auth_db),
|
||||
plugin_loader = Depends(get_plugin_loader),
|
||||
_ = Depends(has_permission("admin:roles", "READ"))
|
||||
):
|
||||
with belief_scope("api.admin.list_permissions"):
|
||||
declared_permissions = discover_declared_permissions(plugin_loader=plugin_loader)
|
||||
inserted_count = sync_permission_catalog(db=db, declared_permissions=declared_permissions)
|
||||
if inserted_count > 0:
|
||||
logger.info(
|
||||
"[api.admin.list_permissions][Action] Synchronized %s missing RBAC permissions into auth catalog",
|
||||
inserted_count,
|
||||
)
|
||||
|
||||
repo = AuthRepository(db)
|
||||
return repo.list_permissions()
|
||||
# [/DEF:list_permissions:Function]
|
||||
|
||||
# [DEF:list_ad_mappings:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Lists all AD Group to Role mappings.
|
||||
@router.get("/ad-mappings", response_model=List[ADGroupMappingSchema])
|
||||
async def list_ad_mappings(
|
||||
@@ -289,6 +323,7 @@ async def list_ad_mappings(
|
||||
# [/DEF:list_ad_mappings:Function]
|
||||
|
||||
# [DEF:create_ad_mapping:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Creates a new AD Group mapping.
|
||||
@router.post("/ad-mappings", response_model=ADGroupMappingSchema)
|
||||
async def create_ad_mapping(
|
||||
@@ -307,4 +342,4 @@ async def create_ad_mapping(
|
||||
return new_mapping
|
||||
# [/DEF:create_ad_mapping:Function]
|
||||
|
||||
# [/DEF:backend.src.api.routes.admin:Module]
|
||||
# [/DEF:AdminApi:Module]
|
||||
@@ -1,10 +1,10 @@
|
||||
# [DEF:backend.src.api.routes.assistant:Module]
|
||||
# @TIER: STANDARD
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: api, assistant, chat, command, confirmation
|
||||
# @PURPOSE: API routes for LLM assistant command parsing and safe execution orchestration.
|
||||
# @LAYER: API
|
||||
# @RELATION: DEPENDS_ON -> backend.src.core.task_manager
|
||||
# @RELATION: DEPENDS_ON -> backend.src.models.assistant
|
||||
# @RELATION: [DEPENDS_ON] ->[backend.src.core.task_manager.manager.TaskManager]
|
||||
# @RELATION: [DEPENDS_ON] ->[backend.src.models.assistant]
|
||||
# @INVARIANT: Risky operations are never executed without valid confirmation token.
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -47,7 +47,7 @@ git_service = GitService()
|
||||
|
||||
|
||||
# [DEF:AssistantMessageRequest:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Input payload for assistant message endpoint.
|
||||
# @PRE: message length is within accepted bounds.
|
||||
# @POST: Request object provides message text and optional conversation binding.
|
||||
@@ -58,7 +58,7 @@ class AssistantMessageRequest(BaseModel):
|
||||
|
||||
|
||||
# [DEF:AssistantAction:Class]
|
||||
# @TIER: TRIVIAL
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: UI action descriptor returned with assistant responses.
|
||||
# @PRE: type and label are provided by orchestration logic.
|
||||
# @POST: Action can be rendered as button on frontend.
|
||||
@@ -70,7 +70,7 @@ class AssistantAction(BaseModel):
|
||||
|
||||
|
||||
# [DEF:AssistantMessageResponse:Class]
|
||||
# @TIER: STANDARD
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Output payload contract for assistant interaction endpoints.
|
||||
# @PRE: Response includes deterministic state and text.
|
||||
# @POST: Payload may include task_id/confirmation_id/actions for UI follow-up.
|
||||
@@ -88,7 +88,7 @@ class AssistantMessageResponse(BaseModel):
|
||||
|
||||
|
||||
# [DEF:ConfirmationRecord:Class]
|
||||
# @TIER: STANDARD
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: In-memory confirmation token model for risky operation dispatch.
|
||||
# @PRE: intent/dispatch/user_id are populated at confirmation request time.
|
||||
# @POST: Record tracks lifecycle state and expiry timestamp.
|
||||
@@ -120,10 +120,12 @@ INTENT_PERMISSION_CHECKS: Dict[str, List[Tuple[str, str]]] = {
|
||||
"run_backup": [("plugin:superset-backup", "EXECUTE"), ("plugin:backup", "EXECUTE")],
|
||||
"run_llm_validation": [("plugin:llm_dashboard_validation", "EXECUTE")],
|
||||
"run_llm_documentation": [("plugin:llm_documentation", "EXECUTE")],
|
||||
"get_health_summary": [("plugin:migration", "READ")],
|
||||
}
|
||||
|
||||
|
||||
# [DEF:_append_history:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Append conversation message to in-memory history buffer.
|
||||
# @PRE: user_id and conversation_id identify target conversation bucket.
|
||||
# @POST: Message entry is appended to CONVERSATIONS key list.
|
||||
@@ -155,6 +157,7 @@ def _append_history(
|
||||
|
||||
|
||||
# [DEF:_persist_message:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist assistant/user message record to database.
|
||||
# @PRE: db session is writable and message payload is serializable.
|
||||
# @POST: Message row is committed or persistence failure is logged.
|
||||
@@ -190,6 +193,7 @@ def _persist_message(
|
||||
|
||||
|
||||
# [DEF:_audit:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Append in-memory audit record for assistant decision trace.
|
||||
# @PRE: payload describes decision/outcome fields.
|
||||
# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry.
|
||||
@@ -202,6 +206,7 @@ def _audit(user_id: str, payload: Dict[str, Any]):
|
||||
|
||||
|
||||
# [DEF:_persist_audit:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist structured assistant audit payload in database.
|
||||
# @PRE: db session is writable and payload is JSON-serializable.
|
||||
# @POST: Audit row is committed or failure is logged with rollback.
|
||||
@@ -225,6 +230,7 @@ def _persist_audit(db: Session, user_id: str, payload: Dict[str, Any], conversat
|
||||
|
||||
|
||||
# [DEF:_persist_confirmation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist confirmation token record to database.
|
||||
# @PRE: record contains id/user/intent/dispatch/expiry fields.
|
||||
# @POST: Confirmation row exists in persistent storage.
|
||||
@@ -250,6 +256,7 @@ def _persist_confirmation(db: Session, record: ConfirmationRecord):
|
||||
|
||||
|
||||
# [DEF:_update_confirmation_state:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Update persistent confirmation token lifecycle state.
|
||||
# @PRE: confirmation_id references existing row.
|
||||
# @POST: State and consumed_at fields are updated when applicable.
|
||||
@@ -269,6 +276,7 @@ def _update_confirmation_state(db: Session, confirmation_id: str, state: str):
|
||||
|
||||
|
||||
# [DEF:_load_confirmation_from_db:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Load confirmation token from database into in-memory model.
|
||||
# @PRE: confirmation_id may or may not exist in storage.
|
||||
# @POST: Returns ConfirmationRecord when found, otherwise None.
|
||||
@@ -294,6 +302,7 @@ def _load_confirmation_from_db(db: Session, confirmation_id: str) -> Optional[Co
|
||||
|
||||
|
||||
# [DEF:_ensure_conversation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve active conversation id in memory or create a new one.
|
||||
# @PRE: user_id identifies current actor.
|
||||
# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.
|
||||
@@ -313,6 +322,7 @@ def _ensure_conversation(user_id: str, conversation_id: Optional[str]) -> str:
|
||||
|
||||
|
||||
# [DEF:_resolve_or_create_conversation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve active conversation using explicit id, memory cache, or persisted history.
|
||||
# @PRE: user_id and db session are available.
|
||||
# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.
|
||||
@@ -342,6 +352,7 @@ def _resolve_or_create_conversation(user_id: str, conversation_id: Optional[str]
|
||||
|
||||
|
||||
# [DEF:_cleanup_history_ttl:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Enforce assistant message retention window by deleting expired rows and in-memory records.
|
||||
# @PRE: db session is available and user_id references current actor scope.
|
||||
# @POST: Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.
|
||||
@@ -379,6 +390,7 @@ def _cleanup_history_ttl(db: Session, user_id: str):
|
||||
|
||||
|
||||
# [DEF:_is_conversation_archived:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Determine archived state for a conversation based on last update timestamp.
|
||||
# @PRE: updated_at can be null for empty conversations.
|
||||
# @POST: Returns True when conversation inactivity exceeds archive threshold.
|
||||
@@ -391,6 +403,7 @@ def _is_conversation_archived(updated_at: Optional[datetime]) -> bool:
|
||||
|
||||
|
||||
# [DEF:_coerce_query_bool:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Normalize bool-like query values for compatibility in direct handler invocations/tests.
|
||||
# @PRE: value may be bool, string, or FastAPI Query metadata object.
|
||||
# @POST: Returns deterministic boolean flag.
|
||||
@@ -404,6 +417,7 @@ def _coerce_query_bool(value: Any) -> bool:
|
||||
|
||||
|
||||
# [DEF:_extract_id:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Extract first regex match group from text by ordered pattern list.
|
||||
# @PRE: patterns contain at least one capture group.
|
||||
# @POST: Returns first matched token or None.
|
||||
@@ -417,6 +431,7 @@ def _extract_id(text: str, patterns: List[str]) -> Optional[str]:
|
||||
|
||||
|
||||
# [DEF:_resolve_env_id:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve environment identifier/name token to canonical environment id.
|
||||
# @PRE: config_manager provides environment list.
|
||||
# @POST: Returns matched environment id or None.
|
||||
@@ -434,6 +449,7 @@ def _resolve_env_id(token: Optional[str], config_manager: ConfigManager) -> Opti
|
||||
|
||||
|
||||
# [DEF:_is_production_env:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Determine whether environment token resolves to production-like target.
|
||||
# @PRE: config_manager provides environments or token text is provided.
|
||||
# @POST: Returns True for production/prod synonyms, else False.
|
||||
@@ -451,6 +467,7 @@ def _is_production_env(token: Optional[str], config_manager: ConfigManager) -> b
|
||||
|
||||
|
||||
# [DEF:_resolve_provider_id:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve provider token to provider id with active/default fallback.
|
||||
# @PRE: db session can load provider list through LLMProviderService.
|
||||
# @POST: Returns provider id or None when no providers configured.
|
||||
@@ -486,6 +503,7 @@ def _resolve_provider_id(
|
||||
|
||||
|
||||
# [DEF:_get_default_environment_id:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve default environment id from settings or first configured environment.
|
||||
# @PRE: config_manager returns environments list.
|
||||
# @POST: Returns default environment id or None when environment list is empty.
|
||||
@@ -507,6 +525,7 @@ def _get_default_environment_id(config_manager: ConfigManager) -> Optional[str]:
|
||||
|
||||
|
||||
# [DEF:_resolve_dashboard_id_by_ref:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve dashboard id by title or slug reference in selected environment.
|
||||
# @PRE: dashboard_ref is a non-empty string-like token.
|
||||
# @POST: Returns dashboard id when uniquely matched, otherwise None.
|
||||
@@ -549,6 +568,7 @@ def _resolve_dashboard_id_by_ref(
|
||||
|
||||
|
||||
# [DEF:_resolve_dashboard_id_entity:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.
|
||||
# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref.
|
||||
# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable.
|
||||
@@ -579,7 +599,142 @@ def _resolve_dashboard_id_entity(
|
||||
# [/DEF:_resolve_dashboard_id_entity:Function]
|
||||
|
||||
|
||||
# [DEF:_get_environment_name_by_id:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @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]
|
||||
# @COMPLEXITY: 3
|
||||
# @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]
|
||||
# @COMPLEXITY: 3
|
||||
# @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]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Deterministically parse RU/EN command text into intent payload.
|
||||
# @PRE: message contains raw user text and config manager resolves environments.
|
||||
# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields.
|
||||
@@ -679,6 +834,9 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any
|
||||
if any(k in lower for k in ["миграц", "migration", "migrate"]):
|
||||
src = _extract_id(lower, [r"(?:с|from)\s+([a-z0-9_-]+)"])
|
||||
tgt = _extract_id(lower, [r"(?:на|to)\s+([a-z0-9_-]+)"])
|
||||
dry_run = "--dry-run" in lower or "dry run" in lower
|
||||
replace_db_config = "--replace-db-config" in lower
|
||||
fix_cross_filters = "--fix-cross-filters" not in lower # Default true usually, but let's say test uses --dry-run
|
||||
is_dangerous = _is_production_env(tgt, config_manager)
|
||||
return {
|
||||
"domain": "migration",
|
||||
@@ -687,10 +845,13 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any
|
||||
"dashboard_id": int(dashboard_id) if dashboard_id else None,
|
||||
"source_env": src,
|
||||
"target_env": tgt,
|
||||
"dry_run": dry_run,
|
||||
"replace_db_config": replace_db_config,
|
||||
"fix_cross_filters": True,
|
||||
},
|
||||
"confidence": 0.95 if dashboard_id and src and tgt else 0.72,
|
||||
"risk_level": "dangerous" if is_dangerous else "guarded",
|
||||
"requires_confirmation": is_dangerous,
|
||||
"requires_confirmation": is_dangerous or dry_run,
|
||||
}
|
||||
|
||||
# Backup
|
||||
@@ -708,6 +869,18 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any
|
||||
"requires_confirmation": False,
|
||||
}
|
||||
|
||||
# Health summary
|
||||
if any(k in lower for k in ["здоровье", "health", "ошибки", "failing", "проблемы"]):
|
||||
env_match = _extract_id(lower, [r"(?:в|for|env|окружени[ея])\s+([a-z0-9_-]+)"])
|
||||
return {
|
||||
"domain": "health",
|
||||
"operation": "get_health_summary",
|
||||
"entities": {"environment": env_match},
|
||||
"confidence": 0.9,
|
||||
"risk_level": "safe",
|
||||
"requires_confirmation": False,
|
||||
}
|
||||
|
||||
# LLM validation
|
||||
if any(k in lower for k in ["валидац", "validate", "провер"]):
|
||||
env_match = _extract_id(lower, [r"(?:в|for|env|окружени[ея])\s+([a-z0-9_-]+)"])
|
||||
@@ -755,6 +928,7 @@ def _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any
|
||||
|
||||
|
||||
# [DEF:_check_any_permission:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Validate user against alternative permission checks (logical OR).
|
||||
# @PRE: checks list contains resource-action tuples.
|
||||
# @POST: Returns on first successful permission; raises 403-like HTTPException otherwise.
|
||||
@@ -772,6 +946,7 @@ def _check_any_permission(current_user: User, checks: List[Tuple[str, str]]):
|
||||
|
||||
|
||||
# [DEF:_has_any_permission:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Check whether user has at least one permission tuple from the provided list.
|
||||
# @PRE: current_user and checks list are valid.
|
||||
# @POST: Returns True when at least one permission check passes.
|
||||
@@ -785,6 +960,7 @@ def _has_any_permission(current_user: User, checks: List[Tuple[str, str]]) -> bo
|
||||
|
||||
|
||||
# [DEF:_build_tool_catalog:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Build current-user tool catalog for LLM planner with operation contracts and defaults.
|
||||
# @PRE: current_user is authenticated; config/db are available.
|
||||
# @POST: Returns list of executable tools filtered by permission and runtime availability.
|
||||
@@ -851,9 +1027,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,
|
||||
},
|
||||
@@ -886,6 +1062,15 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
"risk_level": "guarded",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
{
|
||||
"operation": "get_health_summary",
|
||||
"domain": "health",
|
||||
"description": "Get summary of dashboard health and failing validations",
|
||||
"required_entities": [],
|
||||
"optional_entities": ["environment"],
|
||||
"risk_level": "safe",
|
||||
"requires_confirmation": False,
|
||||
},
|
||||
]
|
||||
|
||||
available: List[Dict[str, Any]] = []
|
||||
@@ -899,6 +1084,7 @@ def _build_tool_catalog(current_user: User, config_manager: ConfigManager, db: S
|
||||
|
||||
|
||||
# [DEF:_coerce_intent_entities:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Normalize intent entity value types from LLM output to route-compatible values.
|
||||
# @PRE: intent contains entities dict or missing entities.
|
||||
# @POST: Returned intent has numeric ids coerced where possible and string values stripped.
|
||||
@@ -918,7 +1104,108 @@ def _coerce_intent_entities(intent: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# [/DEF:_coerce_intent_entities:Function]
|
||||
|
||||
|
||||
# Operations that are read-only and do not require confirmation.
|
||||
_SAFE_OPS = {"show_capabilities", "get_task_status", "get_health_summary"}
|
||||
|
||||
|
||||
# [DEF:_confirmation_summary:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Build human-readable confirmation prompt for an intent before execution.
|
||||
# @PRE: intent contains operation and entities fields.
|
||||
# @POST: Returns descriptive Russian-language text ending with confirmation prompt.
|
||||
async def _async_confirmation_summary(intent: Dict[str, Any], config_manager: ConfigManager, db: Session) -> str:
|
||||
operation = intent.get("operation", "")
|
||||
entities = intent.get("entities", {})
|
||||
descriptions: Dict[str, str] = {
|
||||
"create_branch": "создание ветки{branch} для дашборда{dashboard}",
|
||||
"commit_changes": "коммит изменений для дашборда{dashboard}",
|
||||
"deploy_dashboard": "деплой дашборда{dashboard} в окружение{env}",
|
||||
"execute_migration": "миграция дашборда{dashboard} с{src} на{tgt}",
|
||||
"run_backup": "бэкап окружения{env}{dashboard}",
|
||||
"run_llm_validation": "LLM-валидация дашборда{dashboard}{env}",
|
||||
"run_llm_documentation": "генерация документации для датасета{dataset}{env}",
|
||||
}
|
||||
template = descriptions.get(operation)
|
||||
if not template:
|
||||
return "Подтвердите выполнение операции или отмените."
|
||||
|
||||
def _label(value: Any, prefix: str = " ") -> str:
|
||||
return f"{prefix}{value}" if value else ""
|
||||
|
||||
dashboard = entities.get("dashboard_id") or entities.get("dashboard_ref")
|
||||
text = template.format(
|
||||
branch=_label(entities.get("branch_name")),
|
||||
dashboard=_label(dashboard),
|
||||
env=_label(entities.get("environment") or entities.get("target_env")),
|
||||
src=_label(entities.get("source_env")),
|
||||
tgt=_label(entities.get("target_env")),
|
||||
dataset=_label(entities.get("dataset_id")),
|
||||
)
|
||||
|
||||
if operation == "execute_migration":
|
||||
flags = []
|
||||
flags.append("маппинг БД: " + ("ВКЛ" if _coerce_query_bool(entities.get("replace_db_config", False)) else "ВЫКЛ"))
|
||||
flags.append("исправление кроссфильтров: " + ("ВКЛ" if _coerce_query_bool(entities.get("fix_cross_filters", True)) else "ВЫКЛ"))
|
||||
dry_run_enabled = _coerce_query_bool(entities.get("dry_run", False))
|
||||
flags.append("отчет dry-run: " + ("ВКЛ" if dry_run_enabled else "ВЫКЛ"))
|
||||
text += f" ({', '.join(flags)})"
|
||||
|
||||
if dry_run_enabled:
|
||||
try:
|
||||
from ...core.migration.dry_run_orchestrator import MigrationDryRunService
|
||||
from ...models.dashboard import DashboardSelection
|
||||
from ...core.superset_client import SupersetClient
|
||||
|
||||
src_token = entities.get("source_env")
|
||||
tgt_token = entities.get("target_env")
|
||||
dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)
|
||||
|
||||
if dashboard_id and src_token and tgt_token:
|
||||
src_env_id = _resolve_env_id(src_token, config_manager)
|
||||
tgt_env_id = _resolve_env_id(tgt_token, config_manager)
|
||||
|
||||
if src_env_id and tgt_env_id:
|
||||
env_map = {env.id: env for env in config_manager.get_environments()}
|
||||
source_env = env_map.get(src_env_id)
|
||||
target_env = env_map.get(tgt_env_id)
|
||||
|
||||
if source_env and target_env and source_env.id != target_env.id:
|
||||
selection = DashboardSelection(
|
||||
source_env_id=source_env.id,
|
||||
target_env_id=target_env.id,
|
||||
selected_ids=[dashboard_id],
|
||||
replace_db_config=_coerce_query_bool(entities.get("replace_db_config", False)),
|
||||
fix_cross_filters=_coerce_query_bool(entities.get("fix_cross_filters", True))
|
||||
)
|
||||
service = MigrationDryRunService()
|
||||
source_client = SupersetClient(source_env)
|
||||
target_client = SupersetClient(target_env)
|
||||
report = service.run(selection, source_client, target_client, db)
|
||||
|
||||
s = report.get("summary", {})
|
||||
dash_s = s.get("dashboards", {})
|
||||
charts_s = s.get("charts", {})
|
||||
ds_s = s.get("datasets", {})
|
||||
|
||||
# Determine main actions counts
|
||||
creates = dash_s.get("create", 0) + charts_s.get("create", 0) + ds_s.get("create", 0)
|
||||
updates = dash_s.get("update", 0) + charts_s.get("update", 0) + ds_s.get("update", 0)
|
||||
deletes = dash_s.get("delete", 0) + charts_s.get("delete", 0) + ds_s.get("delete", 0)
|
||||
|
||||
text += f"\n\nОтчет dry-run:\n- Будет создано новых объектов: {creates}\n- Будет обновлено: {updates}\n- Будет удалено: {deletes}"
|
||||
else:
|
||||
text += "\n\n(Не удалось загрузить отчет dry-run: неверные окружения)."
|
||||
except Exception as e:
|
||||
import traceback
|
||||
logger.warning("[assistant.dry_run_summary][failed] Exception: %s\n%s", e, traceback.format_exc())
|
||||
text += f"\n\n(Не удалось загрузить отчет dry-run: {e})."
|
||||
|
||||
return f"Выполнить: {text}. Подтвердите или отмените."
|
||||
# [/DEF:_confirmation_summary:Function]
|
||||
|
||||
|
||||
# [DEF:_clarification_text_for_intent:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Convert technical missing-parameter errors into user-facing clarification prompts.
|
||||
# @PRE: state was classified as needs_clarification for current intent/error combination.
|
||||
# @POST: Returned text is human-readable and actionable for target operation.
|
||||
@@ -942,6 +1229,7 @@ def _clarification_text_for_intent(intent: Optional[Dict[str, Any]], detail_text
|
||||
|
||||
|
||||
# [DEF:_plan_intent_with_llm:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Use active LLM provider to select best tool/operation from dynamic catalog.
|
||||
# @PRE: tools list contains allowed operations for current user.
|
||||
# @POST: Returns normalized intent dict when planning succeeds; otherwise None.
|
||||
@@ -1005,7 +1293,8 @@ async def _plan_intent_with_llm(
|
||||
]
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f"[assistant.planner][fallback] LLM planner unavailable: {exc}")
|
||||
import traceback
|
||||
logger.warning(f"[assistant.planner][fallback] LLM planner unavailable: {exc}\n{traceback.format_exc()}")
|
||||
return None
|
||||
if not isinstance(response, dict):
|
||||
return None
|
||||
@@ -1051,6 +1340,7 @@ async def _plan_intent_with_llm(
|
||||
|
||||
|
||||
# [DEF:_authorize_intent:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Validate user permissions for parsed intent before confirmation/dispatch.
|
||||
# @PRE: intent.operation is present for known assistant command domains.
|
||||
# @POST: Returns if authorized; raises HTTPException(403) when denied.
|
||||
@@ -1062,6 +1352,7 @@ def _authorize_intent(intent: Dict[str, Any], current_user: User):
|
||||
|
||||
|
||||
# [DEF:_dispatch_intent:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Execute parsed assistant intent via existing task/plugin/git services.
|
||||
# @PRE: intent operation is known and actor permissions are validated per operation.
|
||||
# @POST: Returns response text, optional task id, and UI actions for follow-up.
|
||||
@@ -1086,6 +1377,7 @@ async def _dispatch_intent(
|
||||
"run_llm_validation": "LLM: валидация дашборда",
|
||||
"run_llm_documentation": "LLM: генерация документации",
|
||||
"get_task_status": "Статус: проверка задачи",
|
||||
"get_health_summary": "Здоровье: сводка по дашбордам",
|
||||
}
|
||||
available = [labels[t["operation"]] for t in tools_catalog if t["operation"] in labels]
|
||||
if not available:
|
||||
@@ -1098,6 +1390,41 @@ async def _dispatch_intent(
|
||||
)
|
||||
return text, None, []
|
||||
|
||||
if operation == "get_health_summary":
|
||||
from ...services.health_service import HealthService
|
||||
env_token = entities.get("environment")
|
||||
env_id = _resolve_env_id(env_token, config_manager)
|
||||
service = HealthService(db)
|
||||
summary = await service.get_health_summary(environment_id=env_id)
|
||||
|
||||
env_name = _get_environment_name_by_id(env_id, config_manager) if env_id else "всех окружений"
|
||||
text = (
|
||||
f"Сводка здоровья дашбордов для {env_name}:\n"
|
||||
f"- ✅ Прошли проверку: {summary.pass_count}\n"
|
||||
f"- ⚠️ С предупреждениями: {summary.warn_count}\n"
|
||||
f"- ❌ Ошибки валидации: {summary.fail_count}\n"
|
||||
f"- ❓ Неизвестно: {summary.unknown_count}"
|
||||
)
|
||||
|
||||
actions = [
|
||||
AssistantAction(type="open_route", label="Открыть Health Center", target="/dashboards/health")
|
||||
]
|
||||
|
||||
if summary.fail_count > 0:
|
||||
text += "\n\nОбнаружены ошибки в следующих дашбордах:"
|
||||
for item in summary.items:
|
||||
if item.status == "FAIL":
|
||||
text += f"\n- {item.dashboard_id} ({item.environment_id}): {item.summary or 'Нет деталей'}"
|
||||
actions.append(
|
||||
AssistantAction(
|
||||
type="open_route",
|
||||
label=f"Отчет {item.dashboard_id}",
|
||||
target=f"/reports/llm/{item.task_id}"
|
||||
)
|
||||
)
|
||||
|
||||
return text, None, actions[:5] # Limit actions to avoid UI clutter
|
||||
|
||||
if operation == "get_task_status":
|
||||
_check_any_permission(current_user, [("tasks", "READ")])
|
||||
task_id = entities.get("task_id")
|
||||
@@ -1106,19 +1433,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":
|
||||
@@ -1168,20 +1505,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 (
|
||||
@@ -1190,6 +1537,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 []
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1218,6 +1577,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 []
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1303,6 +1674,7 @@ async def _dispatch_intent(
|
||||
|
||||
@router.post("/messages", response_model=AssistantMessageResponse)
|
||||
# [DEF:send_message:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Parse assistant command, enforce safety gates, and dispatch executable intent.
|
||||
# @PRE: Authenticated user is available and message text is non-empty.
|
||||
# @POST: Response state is one of clarification/confirmation/started/success/denied/failed.
|
||||
@@ -1328,14 +1700,7 @@ async def send_message(
|
||||
except Exception as exc:
|
||||
logger.warning(f"[assistant.planner][fallback] Planner error: {exc}")
|
||||
if not intent:
|
||||
intent = {
|
||||
"domain": "unknown",
|
||||
"operation": "clarify",
|
||||
"entities": {},
|
||||
"confidence": 0.0,
|
||||
"risk_level": "safe",
|
||||
"requires_confirmation": False,
|
||||
}
|
||||
intent = _parse_command(request.message, config_manager)
|
||||
confidence = float(intent.get("confidence", 0.0))
|
||||
|
||||
if intent.get("domain") == "unknown" or confidence < 0.6:
|
||||
@@ -1358,7 +1723,8 @@ async def send_message(
|
||||
try:
|
||||
_authorize_intent(intent, current_user)
|
||||
|
||||
if intent.get("requires_confirmation"):
|
||||
operation = intent.get("operation")
|
||||
if operation not in _SAFE_OPS:
|
||||
confirmation_id = str(uuid.uuid4())
|
||||
confirm = ConfirmationRecord(
|
||||
id=confirmation_id,
|
||||
@@ -1371,7 +1737,7 @@ async def send_message(
|
||||
)
|
||||
CONFIRMATIONS[confirmation_id] = confirm
|
||||
_persist_confirmation(db, confirm)
|
||||
text = "Операция рискованная. Подтвердите выполнение или отмените."
|
||||
text = await _async_confirmation_summary(intent, config_manager, db)
|
||||
_append_history(
|
||||
user_id,
|
||||
conversation_id,
|
||||
@@ -1388,7 +1754,13 @@ async def send_message(
|
||||
text,
|
||||
state="needs_confirmation",
|
||||
confirmation_id=confirmation_id,
|
||||
metadata={"intent": intent},
|
||||
metadata={
|
||||
"intent": intent,
|
||||
"actions": [
|
||||
{"type": "confirm", "label": "✅ Подтвердить", "target": confirmation_id},
|
||||
{"type": "cancel", "label": "❌ Отменить", "target": confirmation_id},
|
||||
],
|
||||
},
|
||||
)
|
||||
audit_payload = {
|
||||
"decision": "needs_confirmation",
|
||||
@@ -1406,12 +1778,13 @@ async def send_message(
|
||||
intent=intent,
|
||||
confirmation_id=confirmation_id,
|
||||
actions=[
|
||||
AssistantAction(type="confirm", label="Confirm", target=confirmation_id),
|
||||
AssistantAction(type="cancel", label="Cancel", target=confirmation_id),
|
||||
AssistantAction(type="confirm", label="✅ Подтвердить", target=confirmation_id),
|
||||
AssistantAction(type="cancel", label="❌ Отменить", target=confirmation_id),
|
||||
],
|
||||
created_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
# Read-only operations execute immediately
|
||||
text, task_id, actions = await _dispatch_intent(intent, current_user, task_manager, config_manager, db)
|
||||
state = "started" if task_id else "success"
|
||||
_append_history(user_id, conversation_id, "assistant", text, state=state, task_id=task_id)
|
||||
@@ -1471,6 +1844,7 @@ async def send_message(
|
||||
|
||||
@router.post("/confirmations/{confirmation_id}/confirm", response_model=AssistantMessageResponse)
|
||||
# [DEF:confirm_operation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Execute previously requested risky operation after explicit user confirmation.
|
||||
# @PRE: confirmation_id exists, belongs to current user, is pending, and not expired.
|
||||
# @POST: Confirmation state becomes consumed and operation result is persisted in history.
|
||||
@@ -1537,6 +1911,7 @@ async def confirm_operation(
|
||||
|
||||
@router.post("/confirmations/{confirmation_id}/cancel", response_model=AssistantMessageResponse)
|
||||
# [DEF:cancel_operation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Cancel pending risky operation and mark confirmation token as cancelled.
|
||||
# @PRE: confirmation_id exists, belongs to current user, and is still pending.
|
||||
# @POST: Confirmation becomes cancelled and cannot be executed anymore.
|
||||
@@ -1593,6 +1968,7 @@ async def cancel_operation(
|
||||
|
||||
|
||||
# [DEF:list_conversations:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Return paginated conversation list for current user with archived flag and last message preview.
|
||||
# @PRE: Authenticated user context and valid pagination params.
|
||||
# @POST: Conversations are grouped by conversation_id sorted by latest activity descending.
|
||||
@@ -1679,6 +2055,40 @@ async def list_conversations(
|
||||
# [/DEF:list_conversations:Function]
|
||||
|
||||
|
||||
# [DEF:delete_conversation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Soft-delete or hard-delete a conversation and clear its in-memory trace.
|
||||
# @PRE: conversation_id belongs to current_user.
|
||||
# @POST: Conversation records are removed from DB and CONVERSATIONS cache.
|
||||
@router.delete("/conversations/{conversation_id}")
|
||||
async def delete_conversation(
|
||||
conversation_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
with belief_scope("assistant.conversations.delete"):
|
||||
user_id = current_user.id
|
||||
|
||||
# 1. Remove from in-memory cache
|
||||
key = (user_id, conversation_id)
|
||||
if key in CONVERSATIONS:
|
||||
del CONVERSATIONS[key]
|
||||
|
||||
# 2. Delete from database
|
||||
deleted_count = db.query(AssistantMessageRecord).filter(
|
||||
AssistantMessageRecord.user_id == user_id,
|
||||
AssistantMessageRecord.conversation_id == conversation_id
|
||||
).delete()
|
||||
|
||||
db.commit()
|
||||
|
||||
if deleted_count == 0:
|
||||
raise HTTPException(status_code=404, detail="Conversation not found or already deleted")
|
||||
|
||||
return {"status": "success", "deleted": deleted_count, "conversation_id": conversation_id}
|
||||
# [/DEF:delete_conversation:Function]
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
# [DEF:get_history:Function]
|
||||
# @PURPOSE: Retrieve paginated assistant conversation history for current user.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user