a2c-core 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- a2c_core/__init__.py +3 -0
- a2c_core/contracts/__init__.py +61 -0
- a2c_core/contracts/id_format.py +40 -0
- a2c_core/contracts/inventory.py +60 -0
- a2c_core/contracts/naming.py +40 -0
- a2c_core/contracts/paths.py +48 -0
- a2c_core/contracts/task_ids.py +31 -0
- a2c_core/errors/__init__.py +62 -0
- a2c_core/errors/codes.py +43 -0
- a2c_core/errors/issues.py +24 -0
- a2c_core/schemas/__init__.py +29 -0
- a2c_core/schemas/artifacts.py +54 -0
- a2c_core/schemas/config.py +74 -0
- a2c_core/schemas/metadata.py +45 -0
- a2c_core/schemas/results.py +71 -0
- a2c_core/schemas/workflows_io.py +108 -0
- a2c_core/services/__init__.py +78 -0
- a2c_core/services/discovery.py +23 -0
- a2c_core/services/doctor.py +138 -0
- a2c_core/services/drafts.py +176 -0
- a2c_core/services/epic_drafts.py +155 -0
- a2c_core/services/frontmatter.py +130 -0
- a2c_core/services/inspection.py +147 -0
- a2c_core/services/loader.py +304 -0
- a2c_core/services/repo.py +23 -0
- a2c_core/services/repository.py +22 -0
- a2c_core/services/serialize.py +57 -0
- a2c_core/services/task_intake_drafts.py +181 -0
- a2c_core/services/validation.py +267 -0
- a2c_core/workflows/__init__.py +35 -0
- a2c_core/workflows/create_epic.py +356 -0
- a2c_core/workflows/create_task.py +570 -0
- a2c_core/workflows/decompose_epic.py +406 -0
- a2c_core/workflows/json_extract.py +38 -0
- a2c_core/workflows/prompting.py +126 -0
- a2c_core/workflows/prompts/epic-create.md +30 -0
- a2c_core/workflows/prompts/epic-decompose.md +42 -0
- a2c_core/workflows/prompts/task-create-bug.md +31 -0
- a2c_core/workflows/prompts/task-create.md +30 -0
- a2c_core/workflows/provider_errors.py +16 -0
- a2c_core/workflows/providers/__init__.py +47 -0
- a2c_core/workflows/providers/base.py +58 -0
- a2c_core/workflows/providers/mock.py +203 -0
- a2c_core/workflows/providers/openai_compatible.py +267 -0
- a2c_core/workflows/providers/resolver.py +257 -0
- a2c_core-0.2.1.dist-info/METADATA +42 -0
- a2c_core-0.2.1.dist-info/RECORD +49 -0
- a2c_core-0.2.1.dist-info/WHEEL +5 -0
- a2c_core-0.2.1.dist-info/top_level.txt +1 -0
a2c_core/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Repository artifact contracts — paths, naming, inventory."""
|
|
2
|
+
|
|
3
|
+
from a2c_core.contracts.inventory import M2_ARTIFACTS, ArtifactSpec
|
|
4
|
+
from a2c_core.contracts.naming import (
|
|
5
|
+
ADR_FILENAME_PATTERN,
|
|
6
|
+
ADR_STATUS_PATTERN,
|
|
7
|
+
EPIC_ID_PATTERN,
|
|
8
|
+
SPRINT_ID_PATTERN,
|
|
9
|
+
TASK_ID_PATTERN,
|
|
10
|
+
WORKFLOW_ID_PATTERN,
|
|
11
|
+
is_valid_adr_filename,
|
|
12
|
+
is_valid_epic_id,
|
|
13
|
+
is_valid_sprint_id,
|
|
14
|
+
is_valid_task_id,
|
|
15
|
+
is_valid_workflow_id,
|
|
16
|
+
parse_adr_filename,
|
|
17
|
+
)
|
|
18
|
+
from a2c_core.contracts.paths import (
|
|
19
|
+
A2C_DIR,
|
|
20
|
+
ADR_DIR,
|
|
21
|
+
CONFIG_FILE,
|
|
22
|
+
DOCS_DIR,
|
|
23
|
+
EPICS_DIR,
|
|
24
|
+
PLANNING_DIR,
|
|
25
|
+
ROOT_MARKERS,
|
|
26
|
+
SPRINTS_DIR,
|
|
27
|
+
TASKS_DIR,
|
|
28
|
+
WORKFLOW_MANIFEST,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"A2C_DIR",
|
|
33
|
+
"ADR_DIR",
|
|
34
|
+
"ADR_FILENAME_PATTERN",
|
|
35
|
+
"ADR_STATUS_PATTERN",
|
|
36
|
+
"ARTIFACT_SCHEMA_VERSION",
|
|
37
|
+
"CONFIG_FILE",
|
|
38
|
+
"CONFIG_SCHEMA_VERSION",
|
|
39
|
+
"DOCS_DIR",
|
|
40
|
+
"EPICS_DIR",
|
|
41
|
+
"EPIC_ID_PATTERN",
|
|
42
|
+
"M2_ARTIFACTS",
|
|
43
|
+
"PLANNING_DIR",
|
|
44
|
+
"ROOT_MARKERS",
|
|
45
|
+
"SPRINTS_DIR",
|
|
46
|
+
"SPRINT_ID_PATTERN",
|
|
47
|
+
"TASKS_DIR",
|
|
48
|
+
"TASK_ID_PATTERN",
|
|
49
|
+
"WORKFLOW_ID_PATTERN",
|
|
50
|
+
"WORKFLOW_MANIFEST",
|
|
51
|
+
"ArtifactSpec",
|
|
52
|
+
"is_valid_adr_filename",
|
|
53
|
+
"is_valid_epic_id",
|
|
54
|
+
"is_valid_sprint_id",
|
|
55
|
+
"is_valid_task_id",
|
|
56
|
+
"is_valid_workflow_id",
|
|
57
|
+
"parse_adr_filename",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Re-export version constants from paths
|
|
61
|
+
from a2c_core.contracts.paths import ARTIFACT_SCHEMA_VERSION, CONFIG_SCHEMA_VERSION # noqa: E402
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Planning artifact ID shape and configurable format validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
# Shipped default when id_format.enabled is true and pattern is omitted.
|
|
8
|
+
DEFAULT_ID_FORMAT_PATTERN = r"^[A-Z][A-Z0-9]{1,9}-[0-9]+$"
|
|
9
|
+
|
|
10
|
+
# Loose Jira-like recommendation for non-fatal warnings when strict mode is off.
|
|
11
|
+
RECOMMENDED_ID_SHAPE_PATTERN = r"^[A-Z][A-Z0-9]+-[0-9]+$"
|
|
12
|
+
|
|
13
|
+
_RECOMMENDED_SHAPE = re.compile(RECOMMENDED_ID_SHAPE_PATTERN)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def compile_id_pattern(pattern: str) -> re.Pattern[str]:
|
|
17
|
+
"""Compile a configured ID format pattern."""
|
|
18
|
+
return re.compile(pattern)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def id_matches_pattern(artifact_id: str, pattern: str) -> bool:
|
|
22
|
+
"""Return True when artifact_id matches the configured regex pattern."""
|
|
23
|
+
return bool(compile_id_pattern(pattern).fullmatch(artifact_id))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_recommended_id_shape(artifact_id: str) -> bool:
|
|
27
|
+
"""Return True when artifact_id matches the documented Jira-like recommendation."""
|
|
28
|
+
return bool(_RECOMMENDED_SHAPE.fullmatch(artifact_id))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def looks_like_odd_id(artifact_id: str) -> bool:
|
|
32
|
+
"""Heuristic for IDs that may deserve a shape warning (spaces, empty, etc.)."""
|
|
33
|
+
stripped = artifact_id.strip()
|
|
34
|
+
if not stripped:
|
|
35
|
+
return True
|
|
36
|
+
if stripped != artifact_id:
|
|
37
|
+
return True
|
|
38
|
+
if " " in artifact_id:
|
|
39
|
+
return True
|
|
40
|
+
return not is_recommended_id_shape(artifact_id)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Supported artifact inventory for Milestone 2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class ArtifactSpec:
|
|
10
|
+
kind: str
|
|
11
|
+
relative_dir: str
|
|
12
|
+
filename_pattern: str
|
|
13
|
+
schema_file: str
|
|
14
|
+
model: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
M2_ARTIFACTS: tuple[ArtifactSpec, ...] = (
|
|
18
|
+
ArtifactSpec(
|
|
19
|
+
kind="config",
|
|
20
|
+
relative_dir=".a2c",
|
|
21
|
+
filename_pattern="config.yaml",
|
|
22
|
+
schema_file="config.schema.json",
|
|
23
|
+
model="a2c_core.schemas.config.A2CConfig",
|
|
24
|
+
),
|
|
25
|
+
ArtifactSpec(
|
|
26
|
+
kind="workflow_manifest",
|
|
27
|
+
relative_dir="docs",
|
|
28
|
+
filename_pattern="workflow-manifest.yaml",
|
|
29
|
+
schema_file="workflow-manifest.schema.json",
|
|
30
|
+
model="a2c_core.schemas.metadata.WorkflowManifest",
|
|
31
|
+
),
|
|
32
|
+
ArtifactSpec(
|
|
33
|
+
kind="epic",
|
|
34
|
+
relative_dir="planning/epics",
|
|
35
|
+
filename_pattern="*.md",
|
|
36
|
+
schema_file="epic.schema.json",
|
|
37
|
+
model="a2c_core.schemas.artifacts.Epic",
|
|
38
|
+
),
|
|
39
|
+
ArtifactSpec(
|
|
40
|
+
kind="task",
|
|
41
|
+
relative_dir="planning/tasks",
|
|
42
|
+
filename_pattern="*.md",
|
|
43
|
+
schema_file="task.schema.json",
|
|
44
|
+
model="a2c_core.schemas.artifacts.Task",
|
|
45
|
+
),
|
|
46
|
+
ArtifactSpec(
|
|
47
|
+
kind="sprint",
|
|
48
|
+
relative_dir="planning/sprints",
|
|
49
|
+
filename_pattern="*.md",
|
|
50
|
+
schema_file="sprint.schema.json",
|
|
51
|
+
model="a2c_core.schemas.artifacts.Sprint",
|
|
52
|
+
),
|
|
53
|
+
ArtifactSpec(
|
|
54
|
+
kind="adr",
|
|
55
|
+
relative_dir="docs/adr",
|
|
56
|
+
filename_pattern="NNNN-*.md",
|
|
57
|
+
schema_file="",
|
|
58
|
+
model="a2c_core.schemas.metadata.AdrRecord",
|
|
59
|
+
),
|
|
60
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Identifier and filename conventions (contract C2)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
WORKFLOW_ID_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
8
|
+
EPIC_ID_PATTERN = re.compile(r"^epic-[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
9
|
+
TASK_ID_PATTERN = re.compile(r"^task-[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
10
|
+
SPRINT_ID_PATTERN = re.compile(r"^sprint-[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
11
|
+
ADR_FILENAME_PATTERN = re.compile(r"^\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$")
|
|
12
|
+
ADR_STATUS_PATTERN = re.compile(r"^\s*(?:-\s*)?\*\*Status:\*\*\s*(.+)$", re.MULTILINE)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_valid_workflow_id(value: str) -> bool:
|
|
16
|
+
return bool(WORKFLOW_ID_PATTERN.fullmatch(value))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_valid_epic_id(value: str) -> bool:
|
|
20
|
+
return bool(EPIC_ID_PATTERN.fullmatch(value))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_valid_task_id(value: str) -> bool:
|
|
24
|
+
return bool(TASK_ID_PATTERN.fullmatch(value))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_valid_sprint_id(value: str) -> bool:
|
|
28
|
+
return bool(SPRINT_ID_PATTERN.fullmatch(value))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_valid_adr_filename(name: str) -> bool:
|
|
32
|
+
return bool(ADR_FILENAME_PATTERN.fullmatch(name))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_adr_filename(name: str) -> tuple[int, str] | None:
|
|
36
|
+
if not is_valid_adr_filename(name):
|
|
37
|
+
return None
|
|
38
|
+
number_text, slug_with_ext = name.split("-", 1)
|
|
39
|
+
slug = slug_with_ext.removesuffix(".md")
|
|
40
|
+
return int(number_text), slug
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Repository path conventions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Tool-internal storage (config, caches, indexes — not human-facing planning artifacts)
|
|
8
|
+
A2C_DIR = ".a2c"
|
|
9
|
+
CONFIG_FILE = Path(A2C_DIR) / "config.yaml"
|
|
10
|
+
A2C_CACHE_DIR = Path(A2C_DIR) / "cache"
|
|
11
|
+
DECOMPOSE_EPIC_CACHE_DIR = A2C_CACHE_DIR / "decompose-epic"
|
|
12
|
+
CREATE_EPIC_CACHE_DIR = A2C_CACHE_DIR / "create-epic"
|
|
13
|
+
CREATE_TASK_CACHE_DIR = A2C_CACHE_DIR / "create-task"
|
|
14
|
+
A2C_INDEXES_DIR = Path(A2C_DIR) / "indexes"
|
|
15
|
+
|
|
16
|
+
# Human-facing planning artifacts (Markdown + YAML front matter)
|
|
17
|
+
PLANNING_DIR = Path("planning")
|
|
18
|
+
EPICS_DIR = PLANNING_DIR / "epics"
|
|
19
|
+
TASKS_DIR = PLANNING_DIR / "tasks"
|
|
20
|
+
SPRINTS_DIR = PLANNING_DIR / "sprints"
|
|
21
|
+
|
|
22
|
+
PLANNING_ARTIFACT_EXTENSION = ".md"
|
|
23
|
+
LEGACY_PLANNING_ARTIFACT_EXTENSION = ".yaml"
|
|
24
|
+
|
|
25
|
+
# Deprecated location — transitional loader support only
|
|
26
|
+
LEGACY_PLANNING_DIR = Path(A2C_DIR) / "planning"
|
|
27
|
+
LEGACY_EPICS_DIR = LEGACY_PLANNING_DIR / "epics"
|
|
28
|
+
LEGACY_TASKS_DIR = LEGACY_PLANNING_DIR / "tasks"
|
|
29
|
+
LEGACY_SPRINTS_DIR = LEGACY_PLANNING_DIR / "sprints"
|
|
30
|
+
|
|
31
|
+
DOCS_DIR = Path("docs")
|
|
32
|
+
WORKFLOW_MANIFEST = DOCS_DIR / "workflow-manifest.yaml"
|
|
33
|
+
ADR_DIR = DOCS_DIR / "adr"
|
|
34
|
+
|
|
35
|
+
# Markers used to recognize an A2C-governed repository root
|
|
36
|
+
ROOT_MARKERS = (
|
|
37
|
+
Path("AGENTS.md"),
|
|
38
|
+
WORKFLOW_MANIFEST,
|
|
39
|
+
CONFIG_FILE,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
ARTIFACT_SCHEMA_VERSION = 1
|
|
43
|
+
CONFIG_SCHEMA_VERSION = 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def planning_artifact_path(directory: Path, artifact_id: str) -> str:
|
|
47
|
+
"""Canonical relative path for a planning artifact Markdown file."""
|
|
48
|
+
return str(directory / f"{artifact_id}{PLANNING_ARTIFACT_EXTENSION}")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Task ID helpers for single-task intake."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
PENDING_TASK_ID = "draft-pending"
|
|
8
|
+
_SLUG_WORD_RE = re.compile(r"[a-z0-9]+")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def slug_from_text(text: str, *, max_parts: int = 5) -> str:
|
|
12
|
+
words = _SLUG_WORD_RE.findall(text.lower())
|
|
13
|
+
if not words:
|
|
14
|
+
return "task"
|
|
15
|
+
return "-".join(words[:max_parts])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def draft_key_from_title(title: str) -> str:
|
|
19
|
+
"""Stable cache key for a task intake draft (not the final task id)."""
|
|
20
|
+
return slug_from_text(title)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def allocate_task_id(title: str, *, existing_ids: set[str]) -> str:
|
|
24
|
+
"""Allocate the next collision-free task id from a title slug."""
|
|
25
|
+
base = f"task-{slug_from_text(title)}"
|
|
26
|
+
candidate = base
|
|
27
|
+
suffix = 2
|
|
28
|
+
while candidate in existing_ids:
|
|
29
|
+
candidate = f"{base}-{suffix}"
|
|
30
|
+
suffix += 1
|
|
31
|
+
return candidate
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Typed errors and structured issue records."""
|
|
2
|
+
|
|
3
|
+
from a2c_core.errors.codes import (
|
|
4
|
+
A2C_CONFIG_001,
|
|
5
|
+
A2C_FORMAT_001,
|
|
6
|
+
A2C_FORMAT_002,
|
|
7
|
+
A2C_ID_001,
|
|
8
|
+
A2C_ID_002,
|
|
9
|
+
A2C_ID_003,
|
|
10
|
+
A2C_NAME_001,
|
|
11
|
+
A2C_NAME_002,
|
|
12
|
+
A2C_NAME_003,
|
|
13
|
+
A2C_NAME_004,
|
|
14
|
+
A2C_PROVIDER_001,
|
|
15
|
+
A2C_PROVIDER_002,
|
|
16
|
+
A2C_PROVIDER_003,
|
|
17
|
+
A2C_PROVIDER_004,
|
|
18
|
+
A2C_PROVIDER_005,
|
|
19
|
+
A2C_REPO_001,
|
|
20
|
+
A2C_REPO_002,
|
|
21
|
+
A2C_REPO_003,
|
|
22
|
+
A2C_SCHEMA_001,
|
|
23
|
+
A2C_SCHEMA_002,
|
|
24
|
+
A2C_SCHEMA_003,
|
|
25
|
+
A2C_SCHEMA_004,
|
|
26
|
+
A2C_WORKFLOW_001,
|
|
27
|
+
A2C_WORKFLOW_002,
|
|
28
|
+
A2C_WORKFLOW_003,
|
|
29
|
+
A2C_WORKFLOW_004,
|
|
30
|
+
)
|
|
31
|
+
from a2c_core.errors.issues import Severity, ValidationIssue
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"A2C_CONFIG_001",
|
|
35
|
+
"A2C_FORMAT_001",
|
|
36
|
+
"A2C_FORMAT_002",
|
|
37
|
+
"A2C_ID_001",
|
|
38
|
+
"A2C_ID_002",
|
|
39
|
+
"A2C_ID_003",
|
|
40
|
+
"A2C_NAME_001",
|
|
41
|
+
"A2C_NAME_002",
|
|
42
|
+
"A2C_NAME_003",
|
|
43
|
+
"A2C_NAME_004",
|
|
44
|
+
"A2C_REPO_001",
|
|
45
|
+
"A2C_REPO_002",
|
|
46
|
+
"A2C_REPO_003",
|
|
47
|
+
"A2C_SCHEMA_001",
|
|
48
|
+
"A2C_SCHEMA_002",
|
|
49
|
+
"A2C_SCHEMA_003",
|
|
50
|
+
"A2C_SCHEMA_004",
|
|
51
|
+
"A2C_WORKFLOW_001",
|
|
52
|
+
"A2C_WORKFLOW_002",
|
|
53
|
+
"A2C_WORKFLOW_003",
|
|
54
|
+
"A2C_WORKFLOW_004",
|
|
55
|
+
"A2C_PROVIDER_001",
|
|
56
|
+
"A2C_PROVIDER_002",
|
|
57
|
+
"A2C_PROVIDER_003",
|
|
58
|
+
"A2C_PROVIDER_004",
|
|
59
|
+
"A2C_PROVIDER_005",
|
|
60
|
+
"Severity",
|
|
61
|
+
"ValidationIssue",
|
|
62
|
+
]
|
a2c_core/errors/codes.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Stable A2C error code constants (contract C2)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Schema / parsing
|
|
6
|
+
A2C_SCHEMA_001 = "A2C_SCHEMA_001" # invalid document shape
|
|
7
|
+
A2C_SCHEMA_002 = "A2C_SCHEMA_002" # unsupported document version
|
|
8
|
+
A2C_SCHEMA_003 = "A2C_SCHEMA_003" # YAML/JSON parse failure
|
|
9
|
+
A2C_FORMAT_001 = "A2C_FORMAT_001" # legacy pure-YAML planning artifact
|
|
10
|
+
A2C_FORMAT_002 = "A2C_FORMAT_002" # deprecated .a2c/planning/ location
|
|
11
|
+
|
|
12
|
+
# Naming / identifiers
|
|
13
|
+
A2C_NAME_001 = "A2C_NAME_001" # invalid artifact id
|
|
14
|
+
A2C_NAME_002 = "A2C_NAME_002" # invalid ADR filename
|
|
15
|
+
A2C_NAME_003 = "A2C_NAME_003" # invalid workflow id
|
|
16
|
+
A2C_NAME_004 = "A2C_NAME_004" # filename does not match artifact id (warning)
|
|
17
|
+
|
|
18
|
+
# Planning artifact IDs
|
|
19
|
+
A2C_ID_001 = "A2C_ID_001" # id does not match configured pattern
|
|
20
|
+
A2C_ID_002 = "A2C_ID_002" # duplicate planning artifact id
|
|
21
|
+
A2C_ID_003 = "A2C_ID_003" # recommended id shape suggestion (warning)
|
|
22
|
+
A2C_SCHEMA_004 = "A2C_SCHEMA_004" # planning artifact type mismatch
|
|
23
|
+
|
|
24
|
+
# Repository layout
|
|
25
|
+
A2C_REPO_001 = "A2C_REPO_001" # not an A2C repository root
|
|
26
|
+
A2C_REPO_002 = "A2C_REPO_002" # required artifact missing
|
|
27
|
+
A2C_REPO_003 = "A2C_REPO_003" # artifact reference broken
|
|
28
|
+
|
|
29
|
+
# Config
|
|
30
|
+
A2C_CONFIG_001 = "A2C_CONFIG_001" # invalid config file
|
|
31
|
+
|
|
32
|
+
# AI workflow / decomposition
|
|
33
|
+
A2C_WORKFLOW_001 = "A2C_WORKFLOW_001" # no valid proposal available
|
|
34
|
+
A2C_WORKFLOW_002 = "A2C_WORKFLOW_002" # invalid AI / proposal structure
|
|
35
|
+
A2C_WORKFLOW_003 = "A2C_WORKFLOW_003" # task id conflict on apply
|
|
36
|
+
A2C_WORKFLOW_004 = "A2C_WORKFLOW_004" # epic not found for decomposition
|
|
37
|
+
|
|
38
|
+
# LLM provider integration
|
|
39
|
+
A2C_PROVIDER_001 = "A2C_PROVIDER_001" # provider misconfiguration
|
|
40
|
+
A2C_PROVIDER_002 = "A2C_PROVIDER_002" # provider HTTP / transport failure
|
|
41
|
+
A2C_PROVIDER_003 = "A2C_PROVIDER_003" # provider timeout
|
|
42
|
+
A2C_PROVIDER_004 = "A2C_PROVIDER_004" # unknown provider name
|
|
43
|
+
A2C_PROVIDER_005 = "A2C_PROVIDER_005" # malformed model response
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Structured validation and service issues."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
Severity = Literal["error", "warning"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ValidationIssue:
|
|
13
|
+
"""Machine-consumable issue for CLI/TUI presentation."""
|
|
14
|
+
|
|
15
|
+
code: str
|
|
16
|
+
message: str
|
|
17
|
+
path: str | None = None
|
|
18
|
+
severity: Severity = "error"
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, str]:
|
|
21
|
+
data: dict[str, str] = {"code": self.code, "message": self.message}
|
|
22
|
+
if self.path is not None:
|
|
23
|
+
data["path"] = self.path
|
|
24
|
+
return data
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Pydantic models for A2C repository artifacts and results."""
|
|
2
|
+
|
|
3
|
+
from a2c_core.schemas.artifacts import Epic, EpicStatus, Sprint, SprintStatus, Task, TaskStatus
|
|
4
|
+
from a2c_core.schemas.config import A2CConfig, CoreSettings, LogLevel, WorkflowSettings
|
|
5
|
+
from a2c_core.schemas.metadata import AdrRecord, ProfilePointer, WorkflowManifest, WorkflowOwner
|
|
6
|
+
from a2c_core.schemas.results import CliResultEnvelope, ServiceResult
|
|
7
|
+
from a2c_core.schemas.workflows_io import ApprovedProposal, CommitPlanItem, CommitPlanProposal
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"A2CConfig",
|
|
11
|
+
"AdrRecord",
|
|
12
|
+
"ApprovedProposal",
|
|
13
|
+
"CliResultEnvelope",
|
|
14
|
+
"CommitPlanItem",
|
|
15
|
+
"CommitPlanProposal",
|
|
16
|
+
"CoreSettings",
|
|
17
|
+
"Epic",
|
|
18
|
+
"EpicStatus",
|
|
19
|
+
"LogLevel",
|
|
20
|
+
"ProfilePointer",
|
|
21
|
+
"ServiceResult",
|
|
22
|
+
"Sprint",
|
|
23
|
+
"SprintStatus",
|
|
24
|
+
"Task",
|
|
25
|
+
"TaskStatus",
|
|
26
|
+
"WorkflowManifest",
|
|
27
|
+
"WorkflowOwner",
|
|
28
|
+
"WorkflowSettings",
|
|
29
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Planning artifact models — Epic, Task, Sprint (Milestone 2 minimum set)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
EpicStatus = Literal["draft", "active", "complete", "cancelled"]
|
|
10
|
+
TaskStatus = Literal["todo", "in_progress", "done", "blocked"]
|
|
11
|
+
SprintStatus = Literal["planned", "active", "closed"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Epic(BaseModel):
|
|
15
|
+
"""Epic artifact (`planning/epics/<id>.md` with YAML front matter)."""
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(extra="ignore")
|
|
18
|
+
|
|
19
|
+
version: Literal[1] = 1
|
|
20
|
+
type: Literal["epic"]
|
|
21
|
+
id: str = Field(min_length=1)
|
|
22
|
+
title: str = Field(min_length=1)
|
|
23
|
+
status: EpicStatus = "draft"
|
|
24
|
+
summary: str | None = None
|
|
25
|
+
task_ids: list[str] = Field(default_factory=list)
|
|
26
|
+
body: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Task(BaseModel):
|
|
30
|
+
"""Task artifact (`planning/tasks/<id>.md` with YAML front matter)."""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(extra="ignore")
|
|
33
|
+
|
|
34
|
+
version: Literal[1] = 1
|
|
35
|
+
type: Literal["task"]
|
|
36
|
+
id: str = Field(min_length=1)
|
|
37
|
+
title: str = Field(min_length=1)
|
|
38
|
+
status: TaskStatus = "todo"
|
|
39
|
+
epic_id: str | None = None
|
|
40
|
+
body: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Sprint(BaseModel):
|
|
44
|
+
"""Sprint artifact (`planning/sprints/<id>.md` with YAML front matter)."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(extra="ignore")
|
|
47
|
+
|
|
48
|
+
version: Literal[1] = 1
|
|
49
|
+
type: Literal["sprint"]
|
|
50
|
+
id: str = Field(min_length=1)
|
|
51
|
+
title: str = Field(min_length=1)
|
|
52
|
+
status: SprintStatus = "planned"
|
|
53
|
+
task_ids: list[str] = Field(default_factory=list)
|
|
54
|
+
body: str = ""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Repository configuration models (contract C3)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
9
|
+
|
|
10
|
+
from a2c_core.contracts.id_format import DEFAULT_ID_FORMAT_PATTERN
|
|
11
|
+
|
|
12
|
+
LogLevel = Literal["debug", "info", "warning", "error"]
|
|
13
|
+
DecompositionProviderName = Literal["mock", "openai", "local", "ollama"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CoreSettings(BaseModel):
|
|
17
|
+
"""Core runtime preferences."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra="forbid")
|
|
20
|
+
|
|
21
|
+
log_level: LogLevel = "info"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkflowSettings(BaseModel):
|
|
25
|
+
"""Workflow execution preferences."""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(extra="forbid")
|
|
28
|
+
|
|
29
|
+
approval_required: bool = True
|
|
30
|
+
default_profile: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IdFormatSettings(BaseModel):
|
|
34
|
+
"""Optional strict planning artifact ID regex validation."""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra="forbid")
|
|
37
|
+
|
|
38
|
+
enabled: bool = False
|
|
39
|
+
pattern: str = DEFAULT_ID_FORMAT_PATTERN
|
|
40
|
+
|
|
41
|
+
@field_validator("pattern")
|
|
42
|
+
@classmethod
|
|
43
|
+
def pattern_must_compile(cls, value: str) -> str:
|
|
44
|
+
re.compile(value)
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DecompositionSettings(BaseModel):
|
|
49
|
+
"""Epic decomposition LLM provider settings."""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(extra="forbid")
|
|
52
|
+
|
|
53
|
+
provider: DecompositionProviderName = "mock"
|
|
54
|
+
model: str | None = None
|
|
55
|
+
endpoint: str | None = None
|
|
56
|
+
timeout_seconds: float = Field(default=120.0, gt=0)
|
|
57
|
+
max_tokens: int | None = Field(default=4096, gt=0)
|
|
58
|
+
temperature: float = Field(default=0.2, ge=0.0, le=2.0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class A2CConfig(BaseModel):
|
|
62
|
+
"""Project-local `.a2c/config.yaml` (version 1)."""
|
|
63
|
+
|
|
64
|
+
model_config = ConfigDict(extra="forbid")
|
|
65
|
+
|
|
66
|
+
version: Literal[1] = 1
|
|
67
|
+
core: CoreSettings = Field(default_factory=CoreSettings)
|
|
68
|
+
workflows: WorkflowSettings = Field(default_factory=WorkflowSettings)
|
|
69
|
+
id_format: IdFormatSettings = Field(default_factory=IdFormatSettings)
|
|
70
|
+
decomposition: DecompositionSettings = Field(default_factory=DecompositionSettings)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def defaults(cls) -> A2CConfig:
|
|
74
|
+
return cls()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Workflow and repository metadata models (contract C1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkflowOwner(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
contact: str | None = None
|
|
12
|
+
gitlab_group: str | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WorkflowManifest(BaseModel):
|
|
16
|
+
"""`docs/workflow-manifest.yaml` shape (subset for validation)."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(extra="allow")
|
|
19
|
+
|
|
20
|
+
version: int
|
|
21
|
+
workflow_id: str
|
|
22
|
+
display_title: str | None = None
|
|
23
|
+
source_docs_path: str = "docs"
|
|
24
|
+
required_metadata_files: list[str] = Field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AdrRecord(BaseModel):
|
|
28
|
+
"""Parsed ADR file metadata."""
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(extra="forbid")
|
|
31
|
+
|
|
32
|
+
number: int
|
|
33
|
+
slug: str
|
|
34
|
+
filename: str
|
|
35
|
+
status: str | None = None
|
|
36
|
+
title: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProfilePointer(BaseModel):
|
|
40
|
+
"""Reference to a stack profile."""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(extra="forbid")
|
|
43
|
+
|
|
44
|
+
profile_id: str
|
|
45
|
+
overrides: dict[str, str] = Field(default_factory=dict)
|