Refactor Mattermost and Slack integration workflows to remove legacy Fidelity variables, streamline command execution, and enhance documentation for project profiles. Update scripts and README files to reflect changes in directory structure and configuration precedence, ensuring a consistent approach to project knowledge management across profiles. Improve error handling and validation in profile creation and doctor commands, and enhance test coverage for profile-related functionalities.
This commit is contained in:
@@ -62,7 +62,14 @@ Current fields:
|
||||
}
|
||||
```
|
||||
|
||||
Use `scripts/aiw/profile.py` from new scripts instead of hardcoding root-level project memory or inbox paths.
|
||||
Use `scripts/aiw/profile.py` from new scripts instead of hardcoding project memory or inbox paths.
|
||||
|
||||
Create and validate an isolated profile:
|
||||
|
||||
```bash
|
||||
python3 scripts/aiw/profile.py create my-project --display-name "My Project"
|
||||
python3 scripts/aiw/profile.py doctor --profile my-project
|
||||
```
|
||||
|
||||
## Robustness features
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
"""Profile path resolution for AI Workspace scripts.
|
||||
|
||||
Profiles own their configuration. Reusable scripts should call this module
|
||||
instead of hardcoding root-level project paths.
|
||||
instead of hardcoding project paths.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -22,6 +23,23 @@ DEFAULT_WORKSPACE = {
|
||||
"index_dir": ".aiw/indexes/{profile}",
|
||||
}
|
||||
|
||||
PROFILE_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
||||
KNOWLEDGE_DIRS = [
|
||||
"00-start",
|
||||
"01-current",
|
||||
"02-work-items",
|
||||
"03-context/process",
|
||||
"03-context/systems",
|
||||
"03-context/workstreams",
|
||||
"04-people",
|
||||
"05-decisions",
|
||||
"06-daily",
|
||||
"07-maps",
|
||||
"08-bases",
|
||||
"09-templates",
|
||||
"attachments",
|
||||
]
|
||||
|
||||
|
||||
def workspace_config_path(profile: str, root: Path | None = None) -> Path:
|
||||
base = root or ROOT
|
||||
@@ -73,6 +91,76 @@ def relative_to_root(path: Path, root: Path | None = None) -> str:
|
||||
return str(path)
|
||||
|
||||
|
||||
def validate_profile_name(profile: str) -> None:
|
||||
if not PROFILE_RE.match(profile):
|
||||
raise SystemExit("profile must be lowercase letters, numbers, dashes, or underscores")
|
||||
|
||||
|
||||
def write_file_once(path: Path, text: str) -> None:
|
||||
if path.exists():
|
||||
return
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def create_profile(profile: str, display_name: str | None = None, root: Path | None = None) -> dict[str, Any]:
|
||||
validate_profile_name(profile)
|
||||
base = root or ROOT
|
||||
display = display_name or profile.replace("-", " ").replace("_", " ").title()
|
||||
profile_dir = base / "profiles" / profile
|
||||
knowledge = knowledge_dir(profile, root=base)
|
||||
inbox = inbox_dir(profile, root=base)
|
||||
index = index_dir(profile, root=base)
|
||||
|
||||
if profile_dir.exists() or knowledge.exists() or inbox.exists():
|
||||
raise SystemExit(f"profile already exists or has data directories: {profile}")
|
||||
|
||||
profile_dir.mkdir(parents=True)
|
||||
write_file_once(profile_dir / "profile.md", f"# {display} Profile\n\n## Purpose\n\nDescribe how this project uses AI Workspace.\n\n## Project\n\n- Name: {display}\n- Workspace role: companion AI workspace\n\n## Communication Sources\n\n- Live communication: configure if needed\n- Historical archive: optional\n\n## Work System\n\n- Use the profile project knowledge vault for work items and current state.\n")
|
||||
write_file_once(profile_dir / "workspace.json", json.dumps({
|
||||
"profile": profile,
|
||||
"display_name": display,
|
||||
"description": f"AI Workspace profile for {display}.",
|
||||
"knowledge_dir": relative_to_root(knowledge, root=base),
|
||||
"inbox_dir": relative_to_root(inbox, root=base),
|
||||
"index_dir": relative_to_root(index, root=base),
|
||||
}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
|
||||
write_file_once(profile_dir / "services.json", json.dumps({"profile": profile, "description": f"Local services for {display}.", "services": {}}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
|
||||
write_file_once(profile_dir / "context-sources.json", json.dumps({"communication_sources": {}}, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
for folder in KNOWLEDGE_DIRS:
|
||||
(knowledge / folder).mkdir(parents=True, exist_ok=True)
|
||||
inbox.mkdir(parents=True, exist_ok=True)
|
||||
write_file_once(knowledge / "00-start" / "start-here.md", f"# {display} Start Here\n\nUse this note as the entry point for the {display} project knowledge vault.\n")
|
||||
write_file_once(knowledge / "01-current" / "current-work.md", "# Current Work\n\n## Focus\n\n- TBD\n")
|
||||
write_file_once(knowledge / "01-current" / "work-items.md", "# Work Items\n\nNo active work items recorded yet.\n")
|
||||
write_file_once(knowledge / "03-context" / "project.md", f"# {display} Project Context\n\nDurable project context goes here.\n")
|
||||
write_file_once(knowledge / "04-people" / "index.md", "# People\n\nProject people and role mappings go here.\n")
|
||||
write_file_once(knowledge / "07-maps" / "index.md", "# Maps\n\nNavigation maps go here.\n")
|
||||
write_file_once(knowledge / "09-templates" / "work-item.md", "---\ntype: work-item\ntitle: {{title}}\nupdated: {{date}}\n---\n\n# {{title}}\n")
|
||||
write_file_once(knowledge / "09-templates" / "daily.md", "---\ntype: daily\ndate: {{date}}\nupdated: {{date}}\n---\n\n# {{date}}\n")
|
||||
write_file_once(inbox / "README.md", f"# {display} Inbox\n\nRaw evidence for this profile. Promote durable facts into the project knowledge vault.\n")
|
||||
return {"profile": profile, "profile_dir": relative_to_root(profile_dir, root=base), "knowledge_dir": relative_to_root(knowledge, root=base), "inbox_dir": relative_to_root(inbox, root=base)}
|
||||
|
||||
|
||||
def doctor(profile: str, root: Path | None = None) -> dict[str, Any]:
|
||||
base = root or ROOT
|
||||
config_path = workspace_config_path(profile, root=base)
|
||||
knowledge = knowledge_dir(profile, root=base)
|
||||
inbox = inbox_dir(profile, root=base)
|
||||
checks = {
|
||||
"profile_config_exists": config_path.is_file(),
|
||||
"knowledge_dir_exists": knowledge.is_dir(),
|
||||
"inbox_dir_exists": inbox.is_dir(),
|
||||
"start_here_exists": (knowledge / "00-start" / "start-here.md").is_file(),
|
||||
"current_work_exists": (knowledge / "01-current" / "current-work.md").is_file(),
|
||||
"work_items_exists": (knowledge / "01-current" / "work-items.md").is_file(),
|
||||
"root_project_knowledge_absent": not (base / "project-knowledge").exists(),
|
||||
"root_ai_inbox_absent": not (base / "ai" / "inbox").exists(),
|
||||
}
|
||||
return {"profile": profile, "ok": all(checks.values()), "checks": checks, "paths": {"knowledge_dir": relative_to_root(knowledge, root=base), "inbox_dir": relative_to_root(inbox, root=base), "index_dir": relative_to_root(index_dir(profile, root=base), root=base)}}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
@@ -84,6 +172,13 @@ def main() -> None:
|
||||
config_parser = subparsers.add_parser("config", help="Print resolved workspace configuration as JSON.")
|
||||
config_parser.add_argument("--profile", default="fidelity")
|
||||
|
||||
create_parser = subparsers.add_parser("create", help="Create a new isolated project profile.")
|
||||
create_parser.add_argument("profile")
|
||||
create_parser.add_argument("--display-name", default="")
|
||||
|
||||
doctor_parser = subparsers.add_parser("doctor", help="Validate profile workspace layout.")
|
||||
doctor_parser.add_argument("--profile", default="fidelity")
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command == "path":
|
||||
if args.kind == "knowledge":
|
||||
@@ -94,6 +189,15 @@ def main() -> None:
|
||||
print(index_dir(args.profile))
|
||||
return
|
||||
|
||||
if args.command == "create":
|
||||
print(json.dumps(create_profile(args.profile, args.display_name or None), ensure_ascii=False, indent=2, sort_keys=True))
|
||||
return
|
||||
|
||||
if args.command == "doctor":
|
||||
payload = doctor(args.profile)
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
raise SystemExit(0 if payload["ok"] else 1)
|
||||
|
||||
config = load_workspace_config(args.profile)
|
||||
config["resolved"] = {
|
||||
"knowledge_dir": str(knowledge_dir(args.profile)),
|
||||
|
||||
@@ -48,6 +48,35 @@ class ProfileTests(unittest.TestCase):
|
||||
self.assertEqual(profile.relative_to_root(root / "a" / "b", root=root), "a/b")
|
||||
self.assertEqual(profile.relative_to_root(Path("/external/path"), root=root), "/external/path")
|
||||
|
||||
def test_create_profile_writes_isolated_layout(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
result = profile.create_profile("demo-project", "Demo Project", root=root)
|
||||
|
||||
self.assertEqual(result["knowledge_dir"], "workspaces/demo-project/project-knowledge")
|
||||
self.assertTrue((root / "profiles" / "demo-project" / "workspace.json").is_file())
|
||||
self.assertTrue((root / "workspaces" / "demo-project" / "project-knowledge" / "00-start" / "start-here.md").is_file())
|
||||
self.assertTrue((root / "workspaces" / "demo-project" / "inbox" / "README.md").is_file())
|
||||
|
||||
def test_doctor_reports_clean_layout(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
profile.create_profile("demo", root=root)
|
||||
result = profile.doctor("demo", root=root)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertTrue(result["checks"]["root_project_knowledge_absent"])
|
||||
|
||||
def test_doctor_rejects_root_level_project_data_dirs(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
profile.create_profile("demo", root=root)
|
||||
(root / "project-knowledge").mkdir()
|
||||
result = profile.doctor("demo", root=root)
|
||||
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertFalse(result["checks"]["root_project_knowledge_absent"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user