aid-installer 1.0.0 → 1.1.0
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.
- package/README.md +4 -2
- package/VERSION +1 -1
- package/bin/aid +2444 -193
- package/bin/aid.ps1 +2360 -105
- package/dashboard/home.html +3321 -0
- package/dashboard/index.html +987 -0
- package/dashboard/reader/__init__.py +56 -0
- package/dashboard/reader/derivation.py +892 -0
- package/dashboard/reader/locator.py +228 -0
- package/dashboard/reader/models.py +408 -0
- package/dashboard/reader/parsers.py +2105 -0
- package/dashboard/reader/reader.py +1196 -0
- package/dashboard/server/__init__.py +3 -0
- package/dashboard/server/reader.mjs +3699 -0
- package/dashboard/server/server.mjs +780 -0
- package/dashboard/server/server.py +1004 -0
- package/lib/AidInstallCore.psm1 +446 -43
- package/lib/aid-install-core.sh +405 -48
- package/package.json +5 -2
- package/scripts/postinstall.js +106 -0
- package/scripts/vendor.js +98 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# dashboard/reader/locator.py
|
|
2
|
+
# LC-1 Locator: resolve .aid/ root, enumerate work-NNN-*/ dirs, stat manifest/KB.
|
|
3
|
+
#
|
|
4
|
+
# Responsibility: filesystem listing + read-only git worktree enumeration.
|
|
5
|
+
# No parse (beyond worktree-list output), no write, no derivation.
|
|
6
|
+
# Read-only by construction: stat + iterdir + read-only git worktree list;
|
|
7
|
+
# the git subprocess is delegated to derivation.py (the one module permitted
|
|
8
|
+
# to call subprocess per the existing architecture contract; see FR35).
|
|
9
|
+
# This module does NOT use subprocess directly.
|
|
10
|
+
#
|
|
11
|
+
# Python 3.11+ stdlib only. Zero third-party deps.
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import NamedTuple, Optional
|
|
18
|
+
|
|
19
|
+
# Work-folder glob pattern: exactly work-[0-9]*-* (FR12 / feature-002 LC-1)
|
|
20
|
+
# Matches work-NNN-{slug} directories only; excludes .temp/, .heartbeat/, etc.
|
|
21
|
+
_WORK_GLOB = "work-[0-9]*-*"
|
|
22
|
+
|
|
23
|
+
# Compiled pattern for the same rule (used to double-check against symlinks or files)
|
|
24
|
+
_WORK_RE = re.compile(r"^work-[0-9]+-")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LocatorResult(NamedTuple):
|
|
28
|
+
"""Output of locate_aid_root()."""
|
|
29
|
+
aid_dir: Path # resolved .aid/ path (always set; may not exist)
|
|
30
|
+
aid_exists: bool # whether .aid/ actually exists on disk
|
|
31
|
+
manifest_path: Path # .aid/.aid-manifest.json (may not exist)
|
|
32
|
+
version_path: Path # .aid/.aid-version (may not exist; fallback for ToolInfo)
|
|
33
|
+
settings_path: Path # .aid/settings.yml
|
|
34
|
+
kb_dir: Path # .aid/knowledge/ (may not exist)
|
|
35
|
+
work_dirs: list[Path] # .aid/work-NNN-*/ directories (sorted, dirs only)
|
|
36
|
+
heartbeat_dir: Path # .aid/.heartbeat/ (stat-only; may not exist)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def locate_aid_root(repo_root: str | Path) -> LocatorResult:
|
|
40
|
+
"""Resolve the .aid/ tree from repo_root and enumerate work folders.
|
|
41
|
+
|
|
42
|
+
Structurally excludes .aid/.temp/ and .aid/.heartbeat/ -- only work-NNN-*
|
|
43
|
+
dirs are returned as works (the work-glob is the only filter needed; no
|
|
44
|
+
special-casing of .temp or .heartbeat is required because they don't match
|
|
45
|
+
the glob).
|
|
46
|
+
|
|
47
|
+
Never throws. If .aid/ is absent, aid_exists=False and work_dirs=[].
|
|
48
|
+
"""
|
|
49
|
+
root = Path(repo_root).resolve()
|
|
50
|
+
aid_dir = root / ".aid"
|
|
51
|
+
|
|
52
|
+
manifest_path = aid_dir / ".aid-manifest.json"
|
|
53
|
+
version_path = aid_dir / ".aid-version"
|
|
54
|
+
settings_path = aid_dir / "settings.yml"
|
|
55
|
+
kb_dir = aid_dir / "knowledge"
|
|
56
|
+
heartbeat_dir = aid_dir / ".heartbeat"
|
|
57
|
+
|
|
58
|
+
aid_exists = aid_dir.is_dir()
|
|
59
|
+
work_dirs: list[Path] = []
|
|
60
|
+
|
|
61
|
+
if aid_exists:
|
|
62
|
+
work_dirs = _enumerate_work_dirs(aid_dir)
|
|
63
|
+
|
|
64
|
+
return LocatorResult(
|
|
65
|
+
aid_dir=aid_dir,
|
|
66
|
+
aid_exists=aid_exists,
|
|
67
|
+
manifest_path=manifest_path,
|
|
68
|
+
version_path=version_path,
|
|
69
|
+
settings_path=settings_path,
|
|
70
|
+
kb_dir=kb_dir,
|
|
71
|
+
work_dirs=work_dirs,
|
|
72
|
+
heartbeat_dir=heartbeat_dir,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _enumerate_work_dirs(aid_dir: Path) -> list[Path]:
|
|
77
|
+
"""Return sorted list of .aid/work-NNN-*/ directories.
|
|
78
|
+
|
|
79
|
+
Uses the exact glob work-[0-9]*-* (FR12). Returns only entries that:
|
|
80
|
+
- match the glob
|
|
81
|
+
- are actual directories (not files, not symlinks to non-dirs)
|
|
82
|
+
- match the _WORK_RE pattern (belt-and-suspenders: glob already filters,
|
|
83
|
+
but explicit check guards against oddly-named dirs on case-insensitive FS)
|
|
84
|
+
|
|
85
|
+
.aid/.temp/ and .aid/.heartbeat/ are structurally excluded by the glob --
|
|
86
|
+
they do not start with "work-[0-9]" so they are never yielded.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
candidates = list(aid_dir.glob(_WORK_GLOB))
|
|
90
|
+
except OSError:
|
|
91
|
+
return []
|
|
92
|
+
|
|
93
|
+
result = []
|
|
94
|
+
for p in candidates:
|
|
95
|
+
if p.is_dir() and _WORK_RE.match(p.name):
|
|
96
|
+
result.append(p)
|
|
97
|
+
|
|
98
|
+
result.sort(key=lambda p: p.name)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def stat_path(path: Path) -> Optional[int]:
|
|
103
|
+
"""Return file size in bytes if path exists and is a regular file; else None."""
|
|
104
|
+
try:
|
|
105
|
+
st = path.stat()
|
|
106
|
+
if path.is_file():
|
|
107
|
+
return st.st_size
|
|
108
|
+
return None
|
|
109
|
+
except OSError:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# SD-3: Worktree enumeration (work-004 Pillar 4)
|
|
115
|
+
#
|
|
116
|
+
# The git subprocess calls (worktree list, symbolic-ref) are delegated to
|
|
117
|
+
# derivation.py -- the one reader module permitted to use the subprocess module
|
|
118
|
+
# per the existing architecture contract (FR35, SEC-A1). This module contains
|
|
119
|
+
# only the pure-Python parsing and filesystem-traversal logic; it never calls
|
|
120
|
+
# subprocess directly.
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
# Regex to detect "worktree <path>" lines in --porcelain output.
|
|
124
|
+
_RE_WORKTREE_LINE = re.compile(r"^worktree\s+(.+)$")
|
|
125
|
+
# Regex to detect "branch refs/heads/<name>" lines.
|
|
126
|
+
_RE_BRANCH_LINE = re.compile(r"^branch\s+refs/heads/(.+)$")
|
|
127
|
+
# Label used for detached-HEAD worktrees (no branch line in --porcelain output).
|
|
128
|
+
_DETACHED_LABEL = "(detached)"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def enumerate_worktree_roots(repo_root: str | Path) -> list[tuple[str, Path]]:
|
|
132
|
+
"""Enumerate all persistent git worktrees for repo_root and return per-worktree roots.
|
|
133
|
+
|
|
134
|
+
Delegates the read-only `git -C <root> worktree list --porcelain` call to
|
|
135
|
+
derivation.run_worktree_list() (the fixed-argv / no-shell pattern, twin of
|
|
136
|
+
_run_git_log). The verb is hard-coded in the argv list inside derivation.py;
|
|
137
|
+
no shell and no user-supplied string is ever executed.
|
|
138
|
+
|
|
139
|
+
For each worktree, locates its .aid/ directory (worktree_path/.aid/). The main
|
|
140
|
+
worktree (first record in --porcelain output) is ALWAYS included.
|
|
141
|
+
|
|
142
|
+
Returns a list of (branch_label, aid_dir) pairs, one per worktree. The main
|
|
143
|
+
worktree is always the first element.
|
|
144
|
+
|
|
145
|
+
Degradation (SD-3 DD-A2): git absent / non-git / timeout / parse failure ->
|
|
146
|
+
returns [(main_branch_label, main_aid_dir)] (main-root-only fallback).
|
|
147
|
+
Never throws.
|
|
148
|
+
"""
|
|
149
|
+
# Lazy import to avoid circular imports (locator -> derivation; derivation
|
|
150
|
+
# imports models but NOT locator, so this direction is safe).
|
|
151
|
+
from .derivation import detect_main_branch_label, run_worktree_list # noqa: PLC0415
|
|
152
|
+
|
|
153
|
+
root = Path(repo_root).resolve()
|
|
154
|
+
main_aid = root / ".aid"
|
|
155
|
+
# Main-root-only fallback (returned on any failure mode)
|
|
156
|
+
main_label = detect_main_branch_label(root)
|
|
157
|
+
main_fallback: list[tuple[str, Path]] = [(main_label, main_aid)]
|
|
158
|
+
|
|
159
|
+
porcelain = run_worktree_list(root)
|
|
160
|
+
if porcelain is None:
|
|
161
|
+
return main_fallback
|
|
162
|
+
|
|
163
|
+
parsed = _parse_worktree_porcelain(porcelain)
|
|
164
|
+
if not parsed:
|
|
165
|
+
return main_fallback
|
|
166
|
+
|
|
167
|
+
results: list[tuple[str, Path]] = []
|
|
168
|
+
for wt_path, branch_label in parsed:
|
|
169
|
+
wt_aid = wt_path / ".aid"
|
|
170
|
+
results.append((branch_label, wt_aid))
|
|
171
|
+
|
|
172
|
+
if not results:
|
|
173
|
+
return main_fallback
|
|
174
|
+
|
|
175
|
+
return results
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_worktree_porcelain(output: str) -> list[tuple[Path, str]]:
|
|
179
|
+
"""Parse `git worktree list --porcelain` output into (worktree_path, branch_label) pairs.
|
|
180
|
+
|
|
181
|
+
The --porcelain format groups records by blank lines:
|
|
182
|
+
worktree /abs/path/to/worktree
|
|
183
|
+
HEAD <sha>
|
|
184
|
+
branch refs/heads/<branch> # absent for detached HEADs
|
|
185
|
+
|
|
186
|
+
Returns list of (Path, branch_label) pairs; detached HEADs get label "(detached)".
|
|
187
|
+
Returns [] on any parse failure (caller degrades to main-root-only).
|
|
188
|
+
Never throws. No subprocess calls; no I/O.
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
records: list[tuple[Path, str]] = []
|
|
192
|
+
current_path: Optional[Path] = None
|
|
193
|
+
current_branch: Optional[str] = None
|
|
194
|
+
|
|
195
|
+
for raw_line in output.splitlines():
|
|
196
|
+
line = raw_line.rstrip()
|
|
197
|
+
|
|
198
|
+
if not line:
|
|
199
|
+
# Blank line: flush the current record (if any)
|
|
200
|
+
if current_path is not None:
|
|
201
|
+
label = current_branch if current_branch is not None else _DETACHED_LABEL
|
|
202
|
+
records.append((current_path, label))
|
|
203
|
+
current_path = None
|
|
204
|
+
current_branch = None
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
wt_m = _RE_WORKTREE_LINE.match(line)
|
|
208
|
+
if wt_m:
|
|
209
|
+
# If we have a pending record without a trailing blank, flush it.
|
|
210
|
+
if current_path is not None:
|
|
211
|
+
label = current_branch if current_branch is not None else _DETACHED_LABEL
|
|
212
|
+
records.append((current_path, label))
|
|
213
|
+
current_branch = None
|
|
214
|
+
current_path = Path(wt_m.group(1).strip())
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
br_m = _RE_BRANCH_LINE.match(line)
|
|
218
|
+
if br_m:
|
|
219
|
+
current_branch = br_m.group(1).strip()
|
|
220
|
+
|
|
221
|
+
# Flush any trailing record (output may not end with a blank line)
|
|
222
|
+
if current_path is not None:
|
|
223
|
+
label = current_branch if current_branch is not None else _DETACHED_LABEL
|
|
224
|
+
records.append((current_path, label))
|
|
225
|
+
|
|
226
|
+
return records
|
|
227
|
+
except Exception: # noqa: BLE001 -- never throws
|
|
228
|
+
return []
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# dashboard/reader/models.py
|
|
2
|
+
# Normalized in-memory display model for the AID state reader (feature-002).
|
|
3
|
+
#
|
|
4
|
+
# These types are the single source of truth for the reader's output shape.
|
|
5
|
+
# They mirror the enum vocabularies declared in feature-001's work-state-template.md (DM-6).
|
|
6
|
+
#
|
|
7
|
+
# Python 3.11+ stdlib only. Read-only data records; no persistence, no I/O.
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# DM-6: Enum definitions
|
|
18
|
+
# Single source of truth = feature-001 (canonical/templates/work-state-template.md).
|
|
19
|
+
# Members reproduced here verbatim so the reader's switch is total.
|
|
20
|
+
# The reader adds one reader-only sentinel per enum (Unknown) per DM-6 contract.
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class Lifecycle(str, Enum):
|
|
24
|
+
"""FR16 lifecycle states for a work folder.
|
|
25
|
+
|
|
26
|
+
Members from feature-001 work-state-template.md:
|
|
27
|
+
Running | Paused-Awaiting-Input | Blocked | Completed | Canceled
|
|
28
|
+
Reader-only:
|
|
29
|
+
Unknown -- returned when the normalized block is absent and no fallback fires.
|
|
30
|
+
Never written to disk. See task-011 for the full fallback adapter.
|
|
31
|
+
"""
|
|
32
|
+
Running = "Running"
|
|
33
|
+
PausedAwaitingInput = "Paused-Awaiting-Input"
|
|
34
|
+
Blocked = "Blocked"
|
|
35
|
+
Completed = "Completed"
|
|
36
|
+
Canceled = "Canceled"
|
|
37
|
+
Unknown = "Unknown" # reader-only sentinel; never written to disk
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Phase(str, Enum):
|
|
41
|
+
"""Pipeline phase enum.
|
|
42
|
+
|
|
43
|
+
Members from feature-001 work-state-template.md:
|
|
44
|
+
Interview | Specify | Plan | Detail | Execute | Deploy | Monitor
|
|
45
|
+
Reader-only:
|
|
46
|
+
Unknown -- for an unrecognized Phase literal.
|
|
47
|
+
"""
|
|
48
|
+
Interview = "Interview"
|
|
49
|
+
Specify = "Specify"
|
|
50
|
+
Plan = "Plan"
|
|
51
|
+
Detail = "Detail"
|
|
52
|
+
Execute = "Execute"
|
|
53
|
+
Deploy = "Deploy"
|
|
54
|
+
Monitor = "Monitor"
|
|
55
|
+
Unknown = "Unknown" # reader-only sentinel; never written to disk
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TaskStatus(str, Enum):
|
|
59
|
+
"""Per-task status enum.
|
|
60
|
+
|
|
61
|
+
Members from feature-001 work-state-template.md (closed; single source of truth):
|
|
62
|
+
Pending | In Progress | In Review | Blocked | Done | Failed | Canceled
|
|
63
|
+
Reader-only:
|
|
64
|
+
Unknown -- for a row whose Status string matches no enum member (NFR7: never throws).
|
|
65
|
+
Never written to disk.
|
|
66
|
+
"""
|
|
67
|
+
Pending = "Pending"
|
|
68
|
+
InProgress = "In Progress"
|
|
69
|
+
InReview = "In Review"
|
|
70
|
+
Blocked = "Blocked"
|
|
71
|
+
Done = "Done"
|
|
72
|
+
Failed = "Failed"
|
|
73
|
+
Canceled = "Canceled"
|
|
74
|
+
Unknown = "Unknown" # reader-only sentinel; never written to disk
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SourceMode(str, Enum):
|
|
78
|
+
"""Records which derivation path produced WorkModel.lifecycle."""
|
|
79
|
+
Normalized = "normalized" # ## Pipeline Status block was present
|
|
80
|
+
Fallback = "fallback" # legacy derivation (task-011)
|
|
81
|
+
Mixed = "mixed" # rare: partial migration (task-011)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# DM-2: Level 0 -- ToolInfo
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class ToolInfo:
|
|
90
|
+
"""Level-0 tool / installation metadata parsed from .aid/.aid-manifest.json.
|
|
91
|
+
|
|
92
|
+
manifest_present=False means the manifest file was absent; fields are then None.
|
|
93
|
+
Never errors on absent manifest -- render 'tool info unavailable'.
|
|
94
|
+
"""
|
|
95
|
+
manifest_present: bool
|
|
96
|
+
aid_version: Optional[str] = None
|
|
97
|
+
installed_at: Optional[str] = None
|
|
98
|
+
tools_installed: list[str] = field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# DM-3: Level 1 -- RepoInfo + KbStateRef
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
class KbStatus(str, Enum):
|
|
106
|
+
"""FR32 5-state KB status enum (feature-007 DM-A2).
|
|
107
|
+
|
|
108
|
+
Derived by the reader (FF-A3 waterfall); never written to disk (NFR2).
|
|
109
|
+
Members:
|
|
110
|
+
pending -- .aid/knowledge/ absent or empty
|
|
111
|
+
generating -- KB present but not yet User Approved: yes (safe default, SPEC residual-#1)
|
|
112
|
+
preparing -- KB approved but kb.html absent OR summary not yet V1-approved
|
|
113
|
+
approved -- KB + kb.html ready, current, approved
|
|
114
|
+
outdated -- approved but default branch has advanced past kb_baseline (FR35)
|
|
115
|
+
Reader-only:
|
|
116
|
+
unknown -- un-derivable combination falls back to pending treatment; never thrown
|
|
117
|
+
"""
|
|
118
|
+
pending = "pending"
|
|
119
|
+
generating = "generating"
|
|
120
|
+
preparing = "preparing"
|
|
121
|
+
approved = "approved"
|
|
122
|
+
outdated = "outdated"
|
|
123
|
+
unknown = "unknown" # reader-only sentinel; never written to disk
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class KbBaseline:
|
|
128
|
+
"""Parsed projection of .aid/settings.yml kb_baseline block (DM-A4).
|
|
129
|
+
|
|
130
|
+
branch: the default branch the KB reflects (e.g. 'master'); None if absent
|
|
131
|
+
tip_date: ISO-8601 commit date of that branch's tip at KB generation time; None if absent
|
|
132
|
+
"""
|
|
133
|
+
branch: Optional[str] = None
|
|
134
|
+
tip_date: Optional[str] = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class KbStateRef:
|
|
139
|
+
"""KB state reference (feature-007 DM-A1 extended KbStateRef).
|
|
140
|
+
|
|
141
|
+
Populated from .aid/knowledge/STATE.md + .aid/knowledge/README.md + derivation.
|
|
142
|
+
Absent KB (.aid/knowledge/ missing) -> null in RepoInfo.
|
|
143
|
+
|
|
144
|
+
Retained fields (feature-002 DM-3):
|
|
145
|
+
summary_approved -- from STATE.md ## Knowledge Summary Status **User Approved:** yes/no
|
|
146
|
+
last_summary_date -- parenthesized date on that line
|
|
147
|
+
doc_count -- rows in README.md ## Completeness table
|
|
148
|
+
|
|
149
|
+
New fields (feature-007 DM-A1, task-064):
|
|
150
|
+
status -- FR32 5-state KbStatus (derived, never persisted; NFR2)
|
|
151
|
+
summary_present -- True if <repo>/.aid/dashboard/kb.html exists (stat only)
|
|
152
|
+
kb_baseline -- {branch, tip_date} from .aid/settings.yml; None if unset/unparseable
|
|
153
|
+
"""
|
|
154
|
+
summary_approved: bool
|
|
155
|
+
last_summary_date: Optional[str] = None # from STATE.md "User Approved: yes (YYYY-MM-DD...)"
|
|
156
|
+
doc_count: Optional[int] = None # rows in README.md ## Completeness table
|
|
157
|
+
# feature-007 DM-A1 new fields (task-064):
|
|
158
|
+
status: KbStatus = KbStatus.unknown # FR32 5-state derived status
|
|
159
|
+
summary_present: bool = False # True if .aid/dashboard/kb.html exists
|
|
160
|
+
kb_baseline: Optional[KbBaseline] = None # {branch, tip_date} or None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class RepoInfo:
|
|
165
|
+
"""Level-1 project / .aid/ state."""
|
|
166
|
+
project_name: str # from .aid/settings.yml project.name; fallback: dir basename
|
|
167
|
+
aid_dir: str # resolved .aid/ root path (as string)
|
|
168
|
+
kb_state: Optional[KbStateRef] = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# DM-5: Level 3 -- TaskModel
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class TaskModel:
|
|
177
|
+
"""One row from ## Tasks Status in STATE.md.
|
|
178
|
+
|
|
179
|
+
Table columns (work-state-template.md):
|
|
180
|
+
# | Task | Type | Wave | Status | Review | Elapsed | Notes
|
|
181
|
+
The _none yet_ placeholder row is skipped by the parser.
|
|
182
|
+
|
|
183
|
+
PF-3/PF-5 fields (feature-009, schema_version 3):
|
|
184
|
+
short_name -- parsed from tasks/task-NNN.md first line "# task-NNN: <title>"
|
|
185
|
+
delivery -- integer parsed from STATE Wave "delivery-NNN" (PF-5c; STATE wins)
|
|
186
|
+
lane -- integer derived from PLAN.md wave-map or prose fallback (PF-5a/5b)
|
|
187
|
+
"""
|
|
188
|
+
task_id: str
|
|
189
|
+
type: str
|
|
190
|
+
wave: Optional[str] = None
|
|
191
|
+
status: TaskStatus = TaskStatus.Unknown
|
|
192
|
+
review_grade: Optional[str] = None
|
|
193
|
+
elapsed: Optional[str] = None
|
|
194
|
+
notes: Optional[str] = None
|
|
195
|
+
# schema_version 3 fields (PF-3 / PF-5)
|
|
196
|
+
short_name: Optional[str] = None
|
|
197
|
+
delivery: Optional[int] = None
|
|
198
|
+
lane: Optional[int] = None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# DM-4: Level 2 -- WorkModel (includes PendingInput)
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class PendingInput:
|
|
207
|
+
"""A ### Q{N} entry under ## Cross-phase Q&A (Pending) with Status: Pending."""
|
|
208
|
+
question_id: str # e.g. "Q1"
|
|
209
|
+
category: Optional[str] = None
|
|
210
|
+
impact: Optional[str] = None
|
|
211
|
+
context: Optional[str] = None
|
|
212
|
+
suggested: Optional[str] = None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class FeatureRef:
|
|
217
|
+
"""A single row from ## Features Status in STATE.md (prototype field)."""
|
|
218
|
+
number: int
|
|
219
|
+
name: str
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class DeliverableRef:
|
|
224
|
+
"""A single row from ## Plan / Deliveries in STATE.md (prototype field).
|
|
225
|
+
|
|
226
|
+
delivery_state: the SD-8 lifecycle enum from delivery-NNN/STATE.md ## Delivery Lifecycle
|
|
227
|
+
(Pending-Spec | Specified | Executing | Gated | Done | Blocked).
|
|
228
|
+
None when the delivery STATE.md is absent or the field is unparseable (legacy works).
|
|
229
|
+
"""
|
|
230
|
+
number: int
|
|
231
|
+
name: str
|
|
232
|
+
task_count: int
|
|
233
|
+
delivery_state: Optional[str] = None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class WorkModel:
|
|
238
|
+
"""Level-2 work folder state. One per .aid/work-NNN-*/ directory.
|
|
239
|
+
|
|
240
|
+
Retention = folder persistence (FR12): the model contains exactly the work
|
|
241
|
+
folders that exist on disk; completed and in-flight works are represented
|
|
242
|
+
identically.
|
|
243
|
+
"""
|
|
244
|
+
work_id: str # folder name, e.g. "work-001-aid-dashboard"
|
|
245
|
+
name: str # display label (slug portion)
|
|
246
|
+
lifecycle: Lifecycle = Lifecycle.Unknown
|
|
247
|
+
phase: Optional[Phase] = None
|
|
248
|
+
active_skill: Optional[str] = None
|
|
249
|
+
updated: Optional[str] = None # ISO-8601 or None
|
|
250
|
+
created: Optional[str] = None # date string from ## Lifecycle History "Work created" row
|
|
251
|
+
pause_reason: Optional[str] = None # present only when lifecycle=Paused-Awaiting-Input
|
|
252
|
+
block_reason: Optional[str] = None # present only when lifecycle=Blocked
|
|
253
|
+
block_artifact: Optional[str] = None # e.g. "IMPEDIMENT-task-NNN.md"
|
|
254
|
+
tasks: list[TaskModel] = field(default_factory=list)
|
|
255
|
+
pending_inputs: list[PendingInput] = field(default_factory=list)
|
|
256
|
+
source_mode: SourceMode = SourceMode.Fallback
|
|
257
|
+
# --- prototype: work-overview header fields (delivery-002 prototype) ---
|
|
258
|
+
number: Optional[int] = None # from folder prefix work-NNN-... -> NNN
|
|
259
|
+
title: Optional[str] = None # **Name:** from REQUIREMENTS.md
|
|
260
|
+
description: Optional[str] = None # **Description:** from REQUIREMENTS.md
|
|
261
|
+
objective: Optional[str] = None # body under ## 1. Objective in REQUIREMENTS.md
|
|
262
|
+
work_path: Optional[str] = None # **Path:** from STATE.md ## Triage (full/lite)
|
|
263
|
+
recipe: Optional[str] = None # lite-path recipe from Triage, else None
|
|
264
|
+
features: list[FeatureRef] = field(default_factory=list) # from ## Features Status
|
|
265
|
+
deliverables: list[DeliverableRef] = field(default_factory=list) # from ## Plan / Deliveries
|
|
266
|
+
# work-004 Pillar 4: branch label from the worktree that owns this work folder.
|
|
267
|
+
# "main" for the main worktree; branch name for persistent worktrees; None if unknown.
|
|
268
|
+
branch_label: Optional[str] = None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# DM-1 (feature-008): TaskDetail sub-model (LC-TR, task-069)
|
|
273
|
+
# Populated ONLY for requested task_ids (detail_task_ids param).
|
|
274
|
+
# Never persisted (NFR2); all fields are read-derived.
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
@dataclass
|
|
278
|
+
class Finding:
|
|
279
|
+
"""One bullet under STATE.md ## Quick Check Findings ### task-NNN **Findings:** (DR-2).
|
|
280
|
+
|
|
281
|
+
severity: leading bracketed tag -- [CRITICAL] | [HIGH]; lower/unknown -> [MINOR] neutral
|
|
282
|
+
description: bullet text up to the first ' -- ' (em-dash segment separator)
|
|
283
|
+
location: '{source-file:line}' segment, null if absent
|
|
284
|
+
disposition: trailing 'Fixed-on-spot' / 'Deferred-to-gate' token, null if absent
|
|
285
|
+
reviewer_tier: the block's **Reviewer Tier:** line (always 'Small' for a quick check)
|
|
286
|
+
"""
|
|
287
|
+
severity: str # '[CRITICAL]' | '[HIGH]' | '[MINOR]' (neutral fallback)
|
|
288
|
+
description: str
|
|
289
|
+
location: Optional[str] = None
|
|
290
|
+
disposition: Optional[str] = None
|
|
291
|
+
reviewer_tier: Optional[str] = None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class DeferredIssue:
|
|
296
|
+
"""One row from delivery-NNN-issues.md filtered to Source task == task_id (DR-4).
|
|
297
|
+
|
|
298
|
+
4-col table: Source task | Severity | Description | Status
|
|
299
|
+
status enum: Open | Resolved | Accepted (unknown literal -> neutral, never throws)
|
|
300
|
+
"""
|
|
301
|
+
source_task: str
|
|
302
|
+
severity: str
|
|
303
|
+
description: str
|
|
304
|
+
status: str
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass
|
|
308
|
+
class TaskLedger:
|
|
309
|
+
"""Delivery-level grade context for a task (DR-3/DR-4). NOT a per-task grade.
|
|
310
|
+
|
|
311
|
+
delivery_id: resolved delivery for this task (from ## Tasks Status Wave); null if unassociated
|
|
312
|
+
grade: per-delivery grade from ## Delivery Gates (verbatim, never re-graded -- NFR7)
|
|
313
|
+
reviewer_tier: delivery reviewer tier
|
|
314
|
+
gate_timestamp: when the delivery gate ran
|
|
315
|
+
deferred_issues: rows from delivery-NNN-issues.md where Source task == task_id
|
|
316
|
+
"""
|
|
317
|
+
delivery_id: Optional[str] = None
|
|
318
|
+
grade: Optional[str] = None
|
|
319
|
+
reviewer_tier: Optional[str] = None
|
|
320
|
+
gate_timestamp: Optional[str] = None
|
|
321
|
+
deferred_issues: list[DeferredIssue] = field(default_factory=list)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass
|
|
325
|
+
class RawStateRef:
|
|
326
|
+
"""Verbatim STATE.md bytes the reader ALREADY read this pass (DR-1, DD-3, NFR4).
|
|
327
|
+
|
|
328
|
+
text: whole work STATE.md verbatim (reused from memory, no re-read)
|
|
329
|
+
byte_len: len(text) in bytes (corroborates NFR4 payload budget)
|
|
330
|
+
path: '.aid/{work}/STATE.md' -- read-only caption label, NOT an edit link
|
|
331
|
+
"""
|
|
332
|
+
text: str
|
|
333
|
+
byte_len: int
|
|
334
|
+
path: str
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@dataclass
|
|
338
|
+
class LogAvailability:
|
|
339
|
+
"""Honest DM-4 log inventory for a task (DR-5, KI-008).
|
|
340
|
+
|
|
341
|
+
task_logs: always 'none' (AID persists no per-task execution log)
|
|
342
|
+
server_log_present: stat .aid/.temp/dashboard.log (expected-false on Windows)
|
|
343
|
+
heartbeat_present: stat .aid/.heartbeat/ (liveness signal, corroborating-only, KI-004)
|
|
344
|
+
"""
|
|
345
|
+
task_logs: str = "none" # always "none" today (DM-4)
|
|
346
|
+
server_log_present: bool = False
|
|
347
|
+
heartbeat_present: bool = False
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@dataclass
|
|
351
|
+
class TaskDetail:
|
|
352
|
+
"""Forensic sub-model for one drilled task (DM-1, feature-008 LC-TR).
|
|
353
|
+
|
|
354
|
+
Populated ONLY when detail_task_ids is supplied to read_repo_detail().
|
|
355
|
+
The always-on read_repo() path does NOT populate this; TaskModel is unchanged.
|
|
356
|
+
|
|
357
|
+
task_id: == TaskModel.task_id (the drill key)
|
|
358
|
+
findings: from ## Quick Check Findings ### task-NNN **Findings:** bullets
|
|
359
|
+
ledger: delivery-level grade join (## Delivery Gates + delivery-NNN-issues.md)
|
|
360
|
+
raw_state: the already-read STATE.md bytes (no re-read, NFR4/DD-3)
|
|
361
|
+
logs: honest DM-4 log inventory
|
|
362
|
+
"""
|
|
363
|
+
task_id: str
|
|
364
|
+
findings: list[Finding] = field(default_factory=list)
|
|
365
|
+
ledger: TaskLedger = field(default_factory=TaskLedger)
|
|
366
|
+
raw_state: Optional[RawStateRef] = None
|
|
367
|
+
logs: Optional[LogAvailability] = None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
# DM-7: ReadMeta (provenance of this read pass)
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
@dataclass
|
|
375
|
+
class ReadMeta:
|
|
376
|
+
"""Provenance and health summary for a single read_repo() pass (DM-7).
|
|
377
|
+
|
|
378
|
+
read_at: wall-clock of this pass (the only place the reader reads the clock).
|
|
379
|
+
work_count: works enumerated.
|
|
380
|
+
fallback_works: work_ids whose source_mode != normalized (live tech-debt surface, AC4).
|
|
381
|
+
parse_warnings: non-fatal anomalies (missing section, unparseable row) -- never raised.
|
|
382
|
+
bytes_read: total bytes read across all files (corroborates NFR4 low overhead).
|
|
383
|
+
"""
|
|
384
|
+
read_at: str # ISO-8601
|
|
385
|
+
work_count: int = 0
|
|
386
|
+
fallback_works: list[str] = field(default_factory=list)
|
|
387
|
+
parse_warnings: list[str] = field(default_factory=list)
|
|
388
|
+
bytes_read: int = 0
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
# DM-1: Top-level RepoModel
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
@dataclass
|
|
396
|
+
class RepoModel:
|
|
397
|
+
"""Top-level normalized in-memory display model returned by read_repo().
|
|
398
|
+
|
|
399
|
+
RepoModel
|
|
400
|
+
+-- tool: ToolInfo Level 0 -- machine CLI (FR7)
|
|
401
|
+
+-- repo: RepoInfo Level 1 -- project / .aid/ + KB-state hook
|
|
402
|
+
+-- works: list[WorkModel] Level 2 -- one per .aid/work-NNN-*/ folder (FR12)
|
|
403
|
+
+-- read: ReadMeta provenance of THIS read pass
|
|
404
|
+
"""
|
|
405
|
+
tool: ToolInfo
|
|
406
|
+
repo: RepoInfo
|
|
407
|
+
works: list[WorkModel] = field(default_factory=list)
|
|
408
|
+
read: ReadMeta = field(default_factory=lambda: ReadMeta(read_at=""))
|