From 934f5832da269edc7a4770b1924fd6ac9c8d3e51 Mon Sep 17 00:00:00 2001 From: kprichard Date: Wed, 12 Aug 2026 13:19:43 -0700 Subject: [PATCH] - docs/analysis: the result of prompt- "Answer briefly: I need an analysis of this repo. I need several things- - uml class charts - uml swim lane activity diagrams - mid-to-high level description of how the app works - whatever else you deem helpful Produce PlantUML for charts and diagrams. Store all generated files in ./docs/analysis/ Don't modify existing files. Ignore these paths: - ./build - ./dist - ./libspec.egg-info - ./.git - ./scratch " --- docs/analysis/README.md | 31 +++ docs/analysis/activity_agent_config.puml | 83 +++++++ docs/analysis/activity_agent_config.svg | 2 + docs/analysis/activity_agent_workflow.puml | 127 ++++++++++ docs/analysis/activity_agent_workflow.svg | 2 + docs/analysis/activity_diff_workflow.puml | 90 +++++++ docs/analysis/activity_diff_workflow.svg | 2 + docs/analysis/activity_mcp_server.puml | 74 ++++++ docs/analysis/activity_mcp_server.svg | 1 + docs/analysis/activity_spec_compilation.puml | 74 ++++++ docs/analysis/activity_spec_compilation.svg | 2 + docs/analysis/class_diagram_core.puml | 129 ++++++++++ docs/analysis/class_diagram_core.svg | 1 + docs/analysis/class_diagram_data.puml | 96 ++++++++ docs/analysis/class_diagram_data.svg | 1 + .../class_diagram_infrastructure.puml | 221 ++++++++++++++++++ .../analysis/class_diagram_infrastructure.svg | 1 + docs/analysis/class_diagram_spec_types.puml | 140 +++++++++++ docs/analysis/class_diagram_spec_types.svg | 1 + docs/analysis/overview.md | 219 +++++++++++++++++ 20 files changed, 1297 insertions(+) create mode 100644 docs/analysis/README.md create mode 100644 docs/analysis/activity_agent_config.puml create mode 100644 docs/analysis/activity_agent_config.svg create mode 100644 docs/analysis/activity_agent_workflow.puml create mode 100644 docs/analysis/activity_agent_workflow.svg create mode 100644 docs/analysis/activity_diff_workflow.puml create mode 100644 docs/analysis/activity_diff_workflow.svg create mode 100644 docs/analysis/activity_mcp_server.puml create mode 100644 docs/analysis/activity_mcp_server.svg create mode 100644 docs/analysis/activity_spec_compilation.puml create mode 100644 docs/analysis/activity_spec_compilation.svg create mode 100644 docs/analysis/class_diagram_core.puml create mode 100644 docs/analysis/class_diagram_core.svg create mode 100644 docs/analysis/class_diagram_data.puml create mode 100644 docs/analysis/class_diagram_data.svg create mode 100644 docs/analysis/class_diagram_infrastructure.puml create mode 100644 docs/analysis/class_diagram_infrastructure.svg create mode 100644 docs/analysis/class_diagram_spec_types.puml create mode 100644 docs/analysis/class_diagram_spec_types.svg create mode 100644 docs/analysis/overview.md diff --git a/docs/analysis/README.md b/docs/analysis/README.md new file mode 100644 index 0000000..2b23cf5 --- /dev/null +++ b/docs/analysis/README.md @@ -0,0 +1,31 @@ +# libspec — Analysis Artifacts + +This directory contains auto-generated architectural analysis of the **libspec** project (v10.3.2). + +## Contents + +| File | Description | +|-----------------------------------------------------------------------|-------------| +| [overview.md](overview.md) | Mid-to-high level description of how the application works | +| [class_diagram_core.puml](class_diagram_core.svg) | Core `Spec` / `Ctx` class hierarchy | +| [class_diagram_spec_types.puml](class_diagram_spec_types.svg) | Built-in specification vocabulary types | +| [class_diagram_infrastructure.puml](class_diagram_infrastructure.svg) | CLI, REPL, MCP server, AgentConfig, Watcher | +| [class_diagram_data.puml](class_diagram_data.svg) | Data model classes (Component, Snapshot, Implemented) | +| [activity_spec_compilation.puml](activity_spec_compilation.svg) | How spec files are compiled into Components / XML | +| [activity_diff_workflow.puml](activity_diff_workflow.svg) | Spec diff swim-lane (user ↔ CLI ↔ Git ↔ engine) | +| [activity_mcp_server.puml](activity_mcp_server.svg) | MCP server request-handling swim-lane | +| [activity_agent_workflow.puml](activity_agent_workflow.svg) | Full agent-driven developer workflow swim-lane | +| [activity_agent_config.puml](activity_agent_config.svg) | Agent-config installation swim-lane | + +## Rendering PlantUML + +```bash +# Using the PlantUML JAR +java -jar plantuml.jar docs/analysis/*.puml + +# Or, convert .puml to .svg using NPM's plantuml-cli +$ npm i -g plantuml-cli +$ plantuml-cli --svg docs/analysis/*.puml + +# Or with the VSCode PlantUML extension — open any .puml file and press Alt+D +``` diff --git a/docs/analysis/activity_agent_config.puml b/docs/analysis/activity_agent_config.puml new file mode 100644 index 0000000..055af25 --- /dev/null +++ b/docs/analysis/activity_agent_config.puml @@ -0,0 +1,83 @@ +@startuml activity_agent_config +title libspec — Agent Configuration Installation Swim-lane\n(libspec agent-config ) + +skinparam backgroundColor #FAFAFA +skinparam activity { + BackgroundColor #F0FDF4 + BorderColor #16A34A + ArrowColor #16A34A + FontName Helvetica + StartColor #16A34A + EndColor #16A34A +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +|#E8F5E9|Developer| +start +:Run **libspec agent-config **\n(or call MCP agent_config tool); + +|#EEF2FF|CLI / MCP (cli.py)| +:get_agent_config(agent, project_root)\n→ lookup AgentConfig._registry[agent]; + +|#F5F3FF|AgentConfig (agent_config.py)| +:Instantiate concrete AgentConfig subclass\n(e.g. ClaudeConfig, GeminiConfig, …); +:Resolve uv path (local or PATH); + +' ── Write MCP server config ── +:_backup_if_exists(config_path); +:_load_json/toml_config(config_path); +:Inject mcpServers.libspec entry\n→ {command: uv, args: [run libspec mcp], cwd}; +:_save_json/toml_config(config_path); + +note right + Config locations per agent: + claude → .claude/settings.json + gemini → .gemini/settings.json + antigravity → .gemini/antigravity/mcp_config.json + copilot → .github/mcp.json + codex → .codex/config.toml + opencode → .opencode/opencode.json +end note + +' ── Optional: run agent CLI ── +if (Agent has native CLI?) then (yes) + :Run: antigravity --add-mcp …\nor: gemini mcp add …\netc.; + if (CLI succeeded?) then (yes) + :Mark as configured_via_cli; + else (no / not found) + :Fall back to file-only config; + endif +endif + +' ── Render & install skill ── +:_render_skill()\n→ Jinja2 render of skill.md.j2\nwith agent_id, workflow text; + +|#FFF3E0|skillkit| +:SkillParser().parse_skill_file(temp_path)\n(validate YAML frontmatter); + +|#FFF3E0|skillkit| +if (Validation OK?) then (yes) + |#F3E5F5|Filesystem| + :Rename TEMP_SKILL.md → SKILL.md\nin agent skill dir; +else (no) + |#F5F3FF|AgentConfig (agent_config.py)| + :Raise ValueError\n"Skill Integrity Failure"; + |#E8F5E9|Developer| + :Error shown; + stop +endif + +|#F5F3FF|AgentConfig (agent_config.py)| +:Return success message; + +|#EEF2FF|CLI / MCP (cli.py)| +:Print / return result; + +|#E8F5E9|Developer| +:Agent can now invoke libspec\ntools and follow SKILL.md workflow; +stop + +@enduml diff --git a/docs/analysis/activity_agent_config.svg b/docs/analysis/activity_agent_config.svg new file mode 100644 index 0000000..63a6f6f --- /dev/null +++ b/docs/analysis/activity_agent_config.svg @@ -0,0 +1,2 @@ +libspec — Agent Configuration Installation Swim-lane +(libspec agent-config <agent>)libspec — Agent Configuration Installation Swim-lane(libspec agent-config <agent>)Runlibspec agent-config <agent>(or call MCP agent_config tool)Error shownAgent can now invoke libspectools and follow SKILL.md workflowget_agent_config(agent, project_root)→ lookup AgentConfig._registry[agent]Print / return resultInstantiate concrete AgentConfig subclass(e.g. ClaudeConfig, GeminiConfig, …)Resolve uv path (local or PATH)_backup_if_exists(config_path)_load_json/toml_config(config_path)Inject mcpServers.libspec entry→ {command: uv, args: [run libspec mcp], cwd}Config locations per agent:claude → .claude/settings.jsongemini → .gemini/settings.jsonantigravity → .gemini/antigravity/mcp_config.jsoncopilot → .github/mcp.jsoncodex → .codex/config.tomlopencode → .opencode/opencode.json_save_json/toml_config(config_path)Run: antigravity --add-mcp …or: gemini mcp add …etc.CLI succeeded?yesno / not foundMark as configured_via_cliFall back to file-only configyesAgent has native CLI?_render_skill()→ Jinja2 render of skill.md.j2with agent_id, workflow textRaise ValueError"Skill Integrity Failure"Return success messageSkillParser().parse_skill_file(temp_path)(validate YAML frontmatter)Validation OK?yesnoRename TEMP_SKILL.md → SKILL.mdin agent skill dirDeveloperCLI / MCP (cli.py)AgentConfig (agent_config.py)skillkitFilesystem \ No newline at end of file diff --git a/docs/analysis/activity_agent_workflow.puml b/docs/analysis/activity_agent_workflow.puml new file mode 100644 index 0000000..b0da990 --- /dev/null +++ b/docs/analysis/activity_agent_workflow.puml @@ -0,0 +1,127 @@ +@startuml activity_agent_workflow +title libspec — Full Agent-Driven Developer Workflow Swim-lane\n(8-step spec-driven development cycle) + +skinparam backgroundColor #FAFAFA +skinparam activity { + BackgroundColor #FFF7ED + BorderColor #EA580C + ArrowColor #EA580C + FontName Helvetica + StartColor #EA580C + EndColor #EA580C +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +|#FEF9C3|Developer (Human)| +start +:Decide on a feature\nor bug-fix goal; +:Tell AI agent the task; + +|#FFF7ED|AI Coding Agent| +note right + Agent has read SKILL.md + which contains the 8-step workflow. +end note + +' ──────── STEP 1 ──────── +:STEP 1 — **Edit Spec**\nDecompose broad requirement into\ngranular single-responsibility\nCtx subclasses in spec/*.py; + +|#F0FDF4|Source Code / Tests| +:Write / update spec/*.py\nwith new Requirement / Feature classes; + +' ──────── STEP 2 ──────── +|#FFF7ED|AI Coding Agent| +:STEP 2 — **Diff Spec** (MANDATORY); + +|#EEF2FF|libspec MCP Tools| +:Call **diff** tool\n(or: uv run libspec diff); + +|#F3E5F5|Git| +:compile_live_spec() vs HEAD\ngenerate_native_patch(); + +|#EEF2FF|libspec MCP Tools| +:Return [NEW] / [CHANGED] / [REMOVED]\nrequirement-level patch; + +|#FFF7ED|AI Coding Agent| +if (Any spec changes?) then (yes) + :Review mutations and\nnew requirements; +else (no) + :Specs unchanged;\nNo implementation work needed; + stop +endif + +' ──────── STEP 3 ──────── +:STEP 3 — **Sort Implementation Order**; + +|#EEF2FF|libspec MCP Tools| +:Call **list_dependencies** tool\n(or: uv run libspec dependencies); +:Return component inheritance graph; + +|#FFF7ED|AI Coding Agent| +:Topologically sort components;\nIdentify foundational requirements\nto implement first; + +' ──────── STEP 4 ──────── +:STEP 4 — **Test Driven Development**; + +|#F0FDF4|Source Code / Tests| +:Write failing tests for each\ncomponent in topological order;\nRun test suite → RED; + +' ──────── STEP 5 ──────── +|#FFF7ED|AI Coding Agent| +:STEP 5 — **Implement**; + +|#F0FDF4|Source Code / Tests| +:Implement components to pass tests;\nAdd REQUIREMENT-ID comments\nin source for cross-reference; +:Run test suite → GREEN; + +' ──────── STEP 6 ──────── +|#FFF7ED|AI Coding Agent| +:STEP 6 — **Code Quality**; + +|#F0FDF4|Source Code / Tests| +:Run linter (ruff), formatter,\nstatic analysis (mypy),\ndead-code checks; + +' ──────── STEP 7 ──────── +|#FFF7ED|AI Coding Agent| +:STEP 7 — **Verify Spec Sync**; + +|#EEF2FF|libspec MCP Tools| +:Call **diff** tool again; + +|#F3E5F5|Git| +:Recompile live spec\nvs HEAD; + +|#FFF7ED|AI Coding Agent| +if (All changes\naccounted for?) then (yes) + :Specs and implementation\nare in sync ✓; +else (no) + :Return to STEP 5\n(implement missing items); + note right + Loop until diff shows + no unimplemented changes. + end note + :STEP 5 — Implement; + |#F0FDF4|Source Code / Tests| + :Fill remaining implementation gaps; + |#FFF7ED|AI Coding Agent| + :STEP 7 — Verify Spec Sync (again); +endif + +' ──────── STEP 8 ──────── +:STEP 8 — **Author Git Commit Message**; + +|#FEF9C3|Developer (Human)| +:Review commit message\nand approve; + +|#F3E5F5|Git| +:git commit -m "…"; +:git push; + +|#FEF9C3|Developer (Human)| +:Feature complete ✓; +stop + +@enduml diff --git a/docs/analysis/activity_agent_workflow.svg b/docs/analysis/activity_agent_workflow.svg new file mode 100644 index 0000000..22ff209 --- /dev/null +++ b/docs/analysis/activity_agent_workflow.svg @@ -0,0 +1,2 @@ +libspec — Full Agent-Driven Developer Workflow Swim-lane +(8-step spec-driven development cycle)libspec — Full Agent-Driven Developer Workflow Swim-lane(8-step spec-driven development cycle)Decide on a featureor bug-fix goalTell AI agent the taskReview commit messageand approveFeature complete ✓Agent has read SKILL.mdwhich contains the 8-step workflow.STEP 1 —Edit SpecDecompose broad requirement intogranular single-responsibilityCtx subclasses in spec/*.pySTEP 2 —Diff Spec(MANDATORY)Any spec changes?yesnoReview mutations andnew requirementsSpecs unchanged;No implementation work neededSTEP 3 —Sort Implementation OrderTopologically sort components;Identify foundational requirementsto implement firstSTEP 4 —Test Driven DevelopmentSTEP 5 —ImplementSTEP 6 —Code QualitySTEP 7 —Verify Spec SyncAll changesaccounted for?yesnoSpecs and implementationare in sync ✓Loop until diff showsno unimplemented changes.Return to STEP 5(implement missing items)STEP 5 — ImplementSTEP 7 — Verify Spec Sync (again)STEP 8 —Author Git Commit MessageWrite / update spec/*.pywith new Requirement / Feature classesWrite failing tests for eachcomponent in topological order;Run test suite → REDImplement components to pass tests;Add REQUIREMENT-ID commentsin source for cross-referenceRun test suite → GREENRun linter (ruff), formatter,static analysis (mypy),dead-code checksFill remaining implementation gapsCalldifftool(or: uv run libspec diff)Return [NEW] / [CHANGED] / [REMOVED]requirement-level patchCalllist_dependenciestool(or: uv run libspec dependencies)Return component inheritance graphCalldifftool againcompile_live_spec() vs HEADgenerate_native_patch()Recompile live specvs HEADgit commit -m "…"git pushDeveloper (Human)AI Coding AgentSource Code / Testslibspec MCP ToolsGit \ No newline at end of file diff --git a/docs/analysis/activity_diff_workflow.puml b/docs/analysis/activity_diff_workflow.puml new file mode 100644 index 0000000..e736444 --- /dev/null +++ b/docs/analysis/activity_diff_workflow.puml @@ -0,0 +1,90 @@ +@startuml activity_diff_workflow +title libspec — Spec Diff Swim-lane\n(libspec diff / spec_diff.generate_native_patch) + +skinparam backgroundColor #FAFAFA +skinparam activity { + BackgroundColor #F0FDF4 + BorderColor #16A34A + ArrowColor #16A34A + FontName Helvetica + StartColor #16A34A + EndColor #16A34A +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +|#E8F5E9|User / Agent| +start +:Run **libspec diff [A] [B]**\nor call MCP **diff** tool; + +|#EEF2FF|CLI / MCP (cli.py / mcp_server.py)| +:Validate .libspec/ project marker; +:Call **generate_native_patch**(old, new); + +|#F0FDF4|spec_diff.py| +:Resolve diff targets\n(_resolve_diff_targets); + +if (No args?) then (yes) + :old_commit = "HEAD"\nis_new_pending = True; +else if (One arg?) then (yes) + :new_commit = "HEAD"; +endif + +|#FFF3E0|util.py| +if (new is live?) then (yes) + :compile_live_spec()\n→ new_components; +else (no) + |Git| + :git archive --format=tar {ref} spec\n| tar -xC /tmp/…; + |util.py| + :compile_live_spec(tmpdir)\n→ new_components; +endif + +|#F3E5F5|Git| +:git diff --quiet HEAD -- spec\n(check if live == HEAD); + +|#FFF3E0|util.py| +:compile_git_spec(old_commit)\n→ old_components; + +note right + Git archive extracts spec/ + into a temp dir; compiler + runs there then cleans up. +end note + +|#F0FDF4|spec_diff.py| +:Build old_map & new_map\n{ref → Component}; +:Compute all_refs = union of keys; + +repeat + :Take next ref; + if (only in new?) then (yes) + :Record **[NEW]** entry; + else if (only in old?) then (yes) + :Record **[REMOVED]** entry; + else (changed?) + :_compare_components_natively\n(old, new, maps, cache); + note right + Compares: + 1. docstring (unified diff) + 2. inherits list + 3. recursive parent diffs + end note + if (hash differs?) then (yes) + :Record **[CHANGED]** entry\nwith change detail list; + endif + endif +repeat while (more refs?) is (yes) + +:Collect unresolved_inherited_refs\n(refs in inherits not in new_map); + +|#EEF2FF|CLI / MCP (cli.py / mcp_server.py)| +:Print / return patch output\n([NEW], [REMOVED], [CHANGED],\n[WARNING] unresolved refs); + +|#E8F5E9|User / Agent| +:Review spec drift;\nProceed with implementation; +stop + +@enduml diff --git a/docs/analysis/activity_diff_workflow.svg b/docs/analysis/activity_diff_workflow.svg new file mode 100644 index 0000000..787af9e --- /dev/null +++ b/docs/analysis/activity_diff_workflow.svg @@ -0,0 +1,2 @@ +libspec — Spec Diff Swim-lane +(libspec diff / spec_diff.generate_native_patch)libspec — Spec Diff Swim-lane(libspec diff / spec_diff.generate_native_patch)Runlibspec diff [A] [B]or call MCPdifftoolReview spec drift;Proceed with implementationValidate .libspec/ project markerCallgenerate_native_patch(old, new)Print / return patch output([NEW], [REMOVED], [CHANGED],[WARNING] unresolved refs)Resolve diff targets(_resolve_diff_targets)yesNo args?old_commit = "HEAD"is_new_pending = TrueyesOne arg?new_commit = "HEAD"Build old_map & new_map{ref → Component}Compute all_refs = union of keysTake next refyesonly in new?Record[NEW]entryyesonly in old?changed?Record[REMOVED]entryCompares:1. docstring (unified diff)2. inherits list3. recursive parent diffs_compare_components_natively(old, new, maps, cache)Record[CHANGED]entrywith change detail listyeshash differs?more refs?yesCollect unresolved_inherited_refs(refs in inherits not in new_map)new is live?yesnocompile_live_spec()→ new_componentscompile_live_spec(tmpdir)→ new_componentsGit archive extracts spec/into a temp dir; compilerruns there then cleans up.compile_git_spec(old_commit)→ old_componentsgit archive --format=tar {ref} spec| tar -xC /tmp/…git diffquiet HEADspec(check if live == HEAD)User / AgentCLI / MCP (cli.py / mcp_server.py)spec_diff.pyutil.pyGit \ No newline at end of file diff --git a/docs/analysis/activity_mcp_server.puml b/docs/analysis/activity_mcp_server.puml new file mode 100644 index 0000000..8ef3146 --- /dev/null +++ b/docs/analysis/activity_mcp_server.puml @@ -0,0 +1,74 @@ +@startuml activity_mcp_server +title libspec — MCP Server Request-Handling Swim-lane + +skinparam backgroundColor #FAFAFA +skinparam activity { + BackgroundColor #F5F3FF + BorderColor #7C3AED + ArrowColor #7C3AED + FontName Helvetica + StartColor #7C3AED + EndColor #7C3AED +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +|#EDE9FE|AI Agent (Claude / Gemini / Copilot …)| +start +:Start agent session;\nAgent reads SKILL.md workflow; + +:Call MCP tool via stdio JSON-RPC; + +|#F5F3FF|FastMCP Transport (stdio)| +:Decode JSON-RPC request;\nRoute to registered @mcp.tool(); + +|#EEF2FF|mcp_server.py Tool Handlers| +switch (Which tool?) +case (**diff**) + :subprocess: python -m libspec.cli diff\n[commit_a] [commit_b]; + |spec Engine / util.py| + :generate_native_patch(); + note right + Reuses CLI logic to + avoid duplication. + end note +case (**list_components** / **show_component**) + |spec Engine / util.py| + :compile_live_spec() or\ncompile_git_spec(commit); + :Filter / lookup Component; +case (**search**) + |Git / Filesystem| + :Walk spec/**/*.py and libspec/**/*.py;\nAST parse each file; + |mcp_server.py Tool Handlers| + :Match query in class/function names\nand docstrings; +case (**peek**) + |Git / Filesystem| + :Read file at file_path; + |mcp_server.py Tool Handlers| + :AST parse, find node at (line, char);\nReturn hover + definition JSON; +case (**symbols**) + |Git / Filesystem| + :Read file; + |mcp_server.py Tool Handlers| + :AST walk, collect ClassDef/FunctionDef;\nReturn symbol list JSON; +case (**agent_workflow**) + |spec Engine / util.py| + :resolve_prefix(agent);\nget_agent_workflow(pfx); +case (**agent_config** / **mcp_agent**) + |spec Engine / util.py| + :get_agent_config(agent, root);\nconfiguratorConfigure(); + |Git / Filesystem| + :Write config JSON/TOML;\nInstall SKILL.md; +endswitch + +|#F5F3FF|FastMCP Transport (stdio)| +:Encode JSON-RPC response;\nWrite to stdout; + +|AI Agent (Claude / Gemini / Copilot …)| +:Parse response; +:Continue workflow\n(edit → diff → implement → verify); +stop + +@enduml diff --git a/docs/analysis/activity_mcp_server.svg b/docs/analysis/activity_mcp_server.svg new file mode 100644 index 0000000..9822eec --- /dev/null +++ b/docs/analysis/activity_mcp_server.svg @@ -0,0 +1 @@ +libspec — MCP Server Request-Handling Swim-lanelibspec — MCP Server Request-Handling Swim-laneStart agent session;Agent reads SKILL.md workflowCall MCP tool via stdio JSON-RPCParse responseContinue workflow(edit → diff → implement → verify)Decode JSON-RPC request;Route to registered @mcp.tool()Encode JSON-RPC response;Write to stdoutWhich tool?subprocess: python -m libspec.cli diff[commit_a] [commit_b]Match query in class/function namesand docstringsAST parse, find node at (line, char);Return hover + definition JSONAST walk, collect ClassDef/FunctionDef;Return symbol list JSONReuses CLI logic toavoid duplication.generate_native_patch()compile_live_spec() orcompile_git_spec(commit)Filter / lookup Componentresolve_prefix(agent);get_agent_workflow(pfx)get_agent_config(agent, root);configuratorConfigure()Walk spec//*.py and libspec//*.py;AST parse each fileRead file at file_pathRead fileWrite config JSON/TOML;Install SKILL.mddifflist_components/show_componentsearchpeeksymbolsagent_workflowagent_config/mcp_agentAI Agent (Claude / Gemini / Copilot …)FastMCP Transport (stdio)mcp_server.py Tool Handlersspec Engine / util.pyGit / Filesystem \ No newline at end of file diff --git a/docs/analysis/activity_spec_compilation.puml b/docs/analysis/activity_spec_compilation.puml new file mode 100644 index 0000000..d66dc9b --- /dev/null +++ b/docs/analysis/activity_spec_compilation.puml @@ -0,0 +1,74 @@ +@startuml activity_spec_compilation +title libspec — Spec Compilation Activity\n(Spec.get_components / compile_live_spec) + +skinparam backgroundColor #FAFAFA +skinparam activity { + BackgroundColor #EEF2FF + BorderColor #4F46E5 + FontName Helvetica + ArrowColor #4F46E5 + StartColor #4F46E5 + EndColor #4F46E5 +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +|#E8F5E9|User / Caller| +start +:Call **compile_live_spec()**\nor **Spec().get_components()**; + +|#FFF3E0|util.py| +:Resolve spec file path\n(auto-discover spec/main_spec.py); + +:Compute filesystem fingerprint\n(mtime + size of all spec/*.py); + +if (Cache hit in\n.libspec/cache/live.bin?) then (yes) + :Load cached Component list\nvia marshal; + |#E8F5E9|User / Caller| + :Return **list[Component]**; + stop +else (no) +endif + +|#F3E5F5|Module Importer| +:Clear stale sys.modules\nfor spec package; +:importlib.import_module\n(spec.main_spec); + +|#EEF2FF|Spec Engine (spec.py)| +:Find explicit **Spec** subclass\nin module (or create _ModuleSpec); + +:Call **Spec.modules()** →\nlist of Python modules; + +repeat + :Take next module; + :ctx_spec_classes_in_module(mod)\n→ all Ctx subclasses defined there; + :Instantiate each class → list[Ctx]; +repeat while (more modules?) is (yes) + +note right + **Pass 1**: collect full specs + **Pass 2**: emit inherited + dependency stubs +end note + +:Collect dependency stubs\n(non-root MRO classes with docstrings); + +|#EEF2FF|Spec Engine (spec.py)| +repeat + :Take next Ctx instance; + :Call instance.**ctx()** → template var map; + :Render Jinja2 docstring template\nwith ctx vars; + :SHA-256 hash rendered docstring; + :Build **Component** dataclass\n(ref, docstring, is_template,\ninherits, hash, is_dependency); +repeat while (more instances?) is (yes) + +|#FFF3E0|util.py| +:Write Component list to\n.libspec/cache/live.bin\n(marshal); + +|#E8F5E9|User / Caller| +:Return **list[Component]**; +stop + +@enduml diff --git a/docs/analysis/activity_spec_compilation.svg b/docs/analysis/activity_spec_compilation.svg new file mode 100644 index 0000000..1956ba3 --- /dev/null +++ b/docs/analysis/activity_spec_compilation.svg @@ -0,0 +1,2 @@ +libspec — Spec Compilation Activity +(Spec.get_components / compile_live_spec)libspec — Spec Compilation Activity(Spec.get_components / compile_live_spec)Callcompile_live_spec()orSpec().get_components()Returnlist[Component]Returnlist[Component]Resolve spec file path(auto-discover spec/main_spec.py)Compute filesystem fingerprint(mtime + size of all spec/*.py)Load cached Component listvia marshalyesCache hit in.libspec/cache/live.bin?noWrite Component list to.libspec/cache/live.bin(marshal)Clear stale sys.modulesfor spec packageimportlib.import_module(spec.main_spec)Find explicitSpecsubclassin module (or create _ModuleSpec)CallSpec.modules()list of Python modulesTake next modulectx_spec_classes_in_module(mod)→ all Ctx subclasses defined therePass 1: collect full specsPass 2: emit inheriteddependency stubsInstantiate each class → list[Ctx]more modules?yesCollect dependency stubs(non-root MRO classes with docstrings)Take next Ctx instanceCall instance.ctx()→ template var mapRender Jinja2 docstring templatewith ctx varsSHA-256 hash rendered docstringBuildComponentdataclass(ref, docstring, is_template,inherits, hash, is_dependency)more instances?yesUser / Callerutil.pyModule ImporterSpec Engine (spec.py) \ No newline at end of file diff --git a/docs/analysis/class_diagram_core.puml b/docs/analysis/class_diagram_core.puml new file mode 100644 index 0000000..502f37e --- /dev/null +++ b/docs/analysis/class_diagram_core.puml @@ -0,0 +1,129 @@ +@startuml class_diagram_core +title libspec — Core Engine Class Hierarchy + +skinparam classAttributeIconSize 0 +skinparam monochrome false +skinparam shadowing false +skinparam backgroundColor #FAFAFA +skinparam class { + BackgroundColor #EEF2FF + BorderColor #4F46E5 + ArrowColor #4F46E5 + FontName Helvetica +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +'───────────────────────────────────────── +' Utility / sentinel +'───────────────────────────────────────── +class LazyProxy { + - _load_fn : Callable + - _module : Module | None + + _get_module() : Module + + __getattr__(name) : Any + + __call__(*args, **kwargs) : Any +} + +class _MissingType <> { +} + +'───────────────────────────────────────── +' Core: Spec +'───────────────────────────────────────── +class Spec { + + modules() : list[Module] + + generate_xml() : str + + get_components() : list[Component] + + write_xml(output_dir) : str | None + + handle_cli() : None + -- + - _build_specification_set() : Element + - _append_module_spec_elements(root) : None + - _append_spec(root, element, emitted_refs) : None + - _append_inherited_dependencies(root, spec, emitted_refs) : None + - _dependency_spec_element(cls) : Element + - _source_info_for_class(cls) : dict | None + - _docstring_template_for_class(cls) : str + - _spec_output_path(output_dir, xml_content) : str + - _inject_date_created(xml_content) : str + - _pretty_xml(element) : str +} + +note right of Spec + Orchestrator. Users subclass and + implement modules() to return the + list of Python modules containing + Ctx subclasses. +end note + +'───────────────────────────────────────── +' Core: Ctx +'───────────────────────────────────────── +class Ctx { + + ctx(template_only) : dict + + render_xml() : str + + to_xml_element() : Element + -- + # _non_root_mro_classes() : list[type] + # _inherited_ctx_classes() : list[type] + # _inherited_field_values() : dict + # _expected_template_vars() : set[str] + # _collect_template_context(expected) : dict + # _resolve_template_var(var_name) : Any + # _collect_non_template_context(ctx) : dict + # _delta_requirements() : dict + # _detect_overrides() : list[str] + # _effective_requirement_ids() : list[str] + # _base_template() : str + # _compiled_docstring_template() : str + # _instance_notes() : str + # _source_info(obj) : dict | None + # _to_xml_element(name, value) : Element + # _append_source_metadata(root) : None + # _append_docstring(root, ctx_data) : None + # _append_context(root) : None + # _append_inheritance(root) : None + # _append_effective_req_ids(root) : None + # _append_overrides(root) : None + # _append_delta_requirements(root) : None +} + +note right of Ctx + Base class for ALL spec types. + Docstrings are Jinja2 templates. + ctx() returns the template variable + map for rendering. +end note + +'───────────────────────────────────────── +' Free functions +'───────────────────────────────────────── +class "ctx_spec_classes_in_module(module)" as fn_classes <> { + : list[type[Ctx]] +} + +class "instantiate_module_specs(module)" as fn_instantiate <> { + : list[Ctx] +} + +'───────────────────────────────────────── +' Errors +'───────────────────────────────────────── +class UnimplementedMethodError { +} +UnimplementedMethodError -up-|> Exception + +'───────────────────────────────────────── +' Relationships +'───────────────────────────────────────── +Spec ..> Ctx : "instantiates & processes" +Spec ..> fn_instantiate : "calls" +fn_instantiate ..> fn_classes : "calls" +fn_classes ..> Ctx : "filters subclasses of" +Ctx ..> UnimplementedMethodError : "raises" +Ctx ..> _MissingType : "uses as sentinel" + +@enduml diff --git a/docs/analysis/class_diagram_core.svg b/docs/analysis/class_diagram_core.svg new file mode 100644 index 0000000..210cf4a --- /dev/null +++ b/docs/analysis/class_diagram_core.svg @@ -0,0 +1 @@ +libspec — Core Engine Class Hierarchylibspec — Core Engine Class HierarchyLazyProxy-_load_fn : Callable-_module : Module | None+_get_module() : Module+__getattr__(name) : Any+__call__(*args, **kwargs) : Any«sentinel»_MissingTypeSpec+modules() : list[Module]+generate_xml() : str+get_components() : list[Component]+write_xml(output_dir) : str | None+handle_cli() : None-_build_specification_set() : Element-_append_module_spec_elements(root) : None-_append_spec(root, element, emitted_refs) : None-_append_inherited_dependencies(root, spec, emitted_refs) : None-_dependency_spec_element(cls) : Element-_source_info_for_class(cls) : dict | None-_docstring_template_for_class(cls) : str-_spec_output_path(output_dir, xml_content) : str-_inject_date_created(xml_content) : str-_pretty_xml(element) : strOrchestrator. Users subclass andimplement modules() to return thelist of Python modules containingCtx subclasses.Ctx+ctx(template_only) : dict+render_xml() : str+to_xml_element() : Element#_non_root_mro_classes() : list[type]#_inherited_ctx_classes() : list[type]#_inherited_field_values() : dict#_expected_template_vars() : set[str]#_collect_template_context(expected) : dict#_resolve_template_var(var_name) : Any#_collect_non_template_context(ctx) : dict#_delta_requirements() : dict#_detect_overrides() : list[str]#_effective_requirement_ids() : list[str]#_base_template() : str#_compiled_docstring_template() : str#_instance_notes() : str#_source_info(obj) : dict | None#_to_xml_element(name, value) : Element#_append_source_metadata(root) : None#_append_docstring(root, ctx_data) : None#_append_context(root) : None#_append_inheritance(root) : None#_append_effective_req_ids(root) : None#_append_overrides(root) : None#_append_delta_requirements(root) : NoneBase class for ALL spec types.Docstrings are Jinja2 templates.ctx() returns the template variablemap for rendering.«function»ctx_spec_classes_in_module(module): list[type[Ctx]]«function»instantiate_module_specs(module): list[Ctx]UnimplementedMethodErrorExceptioninstantiates & processescallscallsfilters subclasses ofraisesuses as sentinel \ No newline at end of file diff --git a/docs/analysis/class_diagram_data.puml b/docs/analysis/class_diagram_data.puml new file mode 100644 index 0000000..20638f4 --- /dev/null +++ b/docs/analysis/class_diagram_data.puml @@ -0,0 +1,96 @@ +@startuml class_diagram_data +allow_mixing +title libspec — Data Model (common.py / store.py) + +skinparam classAttributeIconSize 0 +skinparam monochrome false +skinparam shadowing false +skinparam backgroundColor #FAFAFA +skinparam class { + BackgroundColor #FFF7ED + BorderColor #EA580C + ArrowColor #EA580C + FontName Helvetica +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +'───────────────────────────────────────── +' Frozen dataclasses +'───────────────────────────────────────── +class Component <> { + + ref : str + + docstring : str + + is_template : bool + + inherits : list[str] + + hash : str (SHA-256, 64 hex chars) + + is_dependency : bool = False + -- + + __post_init__() : None (strict validation) +} +note right of Component + One Component per compiled + Ctx subclass. hash = SHA-256 + of the rendered docstring. +end note + +class Snapshot <> { + + id : str + + created_at : datetime + + master_hash : str (40 or 64 hex chars) + + git_commit : str | None + -- + + __post_init__() : None +} +note right of Snapshot + Represents the state of the full + specification set at a point in time. + master_hash = sorted XOR of all + Component hashes. +end note + +class Implemented <> { + + ref : str + + spec_hash : str (SHA-256, 64 hex chars) + + file : str + + line : int + + session_id : str | None + -- + + __post_init__() : None +} +note right of Implemented + Records a REQUIREMENT-ID claim + found in source code, linking + implementation back to spec. +end note + +'───────────────────────────────────────── +' Store exceptions +'───────────────────────────────────────── +class StoreError <> { +} +class StoreNotFoundError <> { +} +class SpecStoreNotFoundError <> { +} + +Exception <|-- StoreError +StoreError <|-- StoreNotFoundError +StoreNotFoundError <|-- SpecStoreNotFoundError + +'───────────────────────────────────────── +' Util errors +'───────────────────────────────────────── +class NotALibspecProjectError <> { +} +Exception <|-- NotALibspecProjectError + +'───────────────────────────────────────── +' Relationships +'───────────────────────────────────────── +Snapshot "1" o-- "0..*" Component : "< contains" +Implemented "0..*" ..> Component : "references by ref >" + +@enduml diff --git a/docs/analysis/class_diagram_data.svg b/docs/analysis/class_diagram_data.svg new file mode 100644 index 0000000..5e6605e --- /dev/null +++ b/docs/analysis/class_diagram_data.svg @@ -0,0 +1 @@ +libspec — Data Model (common.py / store.py)libspec — Data Model (common.py / store.py)«dataclass, frozen»Component+ref : str+docstring : str+is_template : bool+inherits : list[str]+hash : str (SHA-256, 64 hex chars)+is_dependency : bool = False+__post_init__() : None (strict validation)One Component per compiledCtx subclass. hash = SHA-256of the rendered docstring.«dataclass, frozen»Snapshot+id : str+created_at : datetime+master_hash : str (40 or 64 hex chars)+git_commit : str | None+__post_init__() : NoneRepresents the state of the fullspecification set at a point in time.master_hash = sorted XOR of allComponent hashes.«dataclass, frozen»Implemented+ref : str+spec_hash : str (SHA-256, 64 hex chars)+file : str+line : int+session_id : str | None+__post_init__() : NoneRecords a REQUIREMENT-ID claimfound in source code, linkingimplementation back to spec.«exception»StoreError«exception»StoreNotFoundError«exception»SpecStoreNotFoundErrorException«exception»NotALibspecProjectErrorcontains10..*references by ref0..* \ No newline at end of file diff --git a/docs/analysis/class_diagram_infrastructure.puml b/docs/analysis/class_diagram_infrastructure.puml new file mode 100644 index 0000000..2eebb9e --- /dev/null +++ b/docs/analysis/class_diagram_infrastructure.puml @@ -0,0 +1,221 @@ +@startuml class_diagram_infrastructure +title libspec — Infrastructure Classes (CLI, REPL, MCP, AgentConfig, Watcher) + +skinparam classAttributeIconSize 0 +skinparam monochrome false +skinparam shadowing false +skinparam backgroundColor #FAFAFA +skinparam class { + BackgroundColor #F5F3FF + BorderColor #7C3AED + ArrowColor #7C3AED + FontName Helvetica +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +'───────────────────────────────────────── +' CLI (cli.py) +'───────────────────────────────────────── +package "CLI (cli.py)" #EDE9FE { + class "main (click group)" as CLI { + + init() + + diff(commit_a, commit_b) + + mcp() + + mcp_agent(agent, project_root, list_agents) + + agent_config(agent, project_root, list_agents) + + repl() + + list(commit_ref) + + show(component_ref, commit_ref) + + search(query, commit_ref) + + log() + + dependencies(commit_ref) + + agent_workflow_cmd(agent, prefix) + + completion_cmd(shell) + } + note right of CLI + Click-based entry point. + Registered as "libspec" script. + end note +} + +'───────────────────────────────────────── +' REPL (repl.py) +'───────────────────────────────────────── +package "REPL (repl.py)" #EDE9FE { + abstract class ReplCommand { + + name() : str [abstract] + + desc() : str [abstract] + + run(repl, arg) : bool [abstract] + + usage() : str + } + + class HelpCommand + class ListCommand + class ShowCommand + class SearchCommand + class DiffCommand + class LogCommand + class DependenciesCommand + class QuitCommand + + class ReplCommander { + + commands : dict[str, ReplCommand] + + register(cmd) : None + + dispatch(repl, line) : bool + } + + class LibspecRepl { + + components : list[Component] + + active_session_id : str | None + + commander : ReplCommander + - _watcher : InotifyFileWatcher | None + -- + + start() : None + + load_components() : None + + active_session_str() : str + - _build_prompt_session() : PromptSession + - _setup_watcher() : None + - _handle_input(text) : bool + } + + ReplCommand <|-- HelpCommand + ReplCommand <|-- ListCommand + ReplCommand <|-- ShowCommand + ReplCommand <|-- SearchCommand + ReplCommand <|-- DiffCommand + ReplCommand <|-- LogCommand + ReplCommand <|-- DependenciesCommand + ReplCommand <|-- QuitCommand + + LibspecRepl *-- ReplCommander + ReplCommander o-- ReplCommand +} + +'───────────────────────────────────────── +' MCP Server (mcp_server.py) +'───────────────────────────────────────── +package "MCP Server (mcp_server.py)" #EDE9FE { + class FastMCPServer <> { + + diff(commit_a, commit_b) : str + + search(query) : str + + peek(file_path, line, character) : str + + usage(file_path, line, character) : str + + symbols(file_path) : str + + mcp_agent(agent, project_root, list_agents) : str + + agent_config(agent, project_root, list_agents) : str + + list_components(commit) : str + + show_component(component_ref, commit) : str + + list_dependencies(commit) : str + + agent_workflow(agent, prefix) : str + -- + + main() : None + } + note right of FastMCPServer + Registered as "libspec-mcp" script. + Transport: stdio (MCP protocol). + end note +} + +'───────────────────────────────────────── +' AgentConfig (agent_config.py) +'───────────────────────────────────────── +package "AgentConfig (agent_config.py)" #EDE9FE { + abstract class AgentConfig { + + project_root : str + + uv_path : str + + mcp_command : dict + + {static} _registry : dict[str, type] + -- + + configure() : str [abstract] + + agent_id : str [abstract] + + agent_display_name : str [abstract] + + agent_description : str [abstract] + + skill_dir_path : str + + is_active : bool + + is_skill_up_to_date() : bool + - _render_skill() : str + - _install_skill(dir_path, content) : None + - _backup_if_exists(config_path) : None + - _load_json_config(path) : dict + - _save_json_config(path, config) : None + - _load_toml_config(path) : dict + - _save_toml_config(path, config) : None + } + + class AntigravityConfig { + agent_id = "antigravity" + } + class GeminiConfig { + agent_id = "gemini" + } + class ClaudeConfig { + agent_id = "claude" + } + class CopilotConfig { + agent_id = "copilot" + } + class CodexConfig { + agent_id = "codex" + } + class OpenCodeConfig { + agent_id = "opencode" + } + + AgentConfig <|-- AntigravityConfig + AgentConfig <|-- GeminiConfig + AgentConfig <|-- ClaudeConfig + AgentConfig <|-- CopilotConfig + AgentConfig <|-- CodexConfig + AgentConfig <|-- OpenCodeConfig +} + +'───────────────────────────────────────── +' Watcher (watcher.py) +'───────────────────────────────────────── +package "Watcher (watcher.py)" #EDE9FE { + abstract class BaseFileWatcher { + + paths : list[str] + + on_change : Callable + -- + + start() : None [abstract] + + stop() : None [abstract] + } + + class InotifyFileWatcher { + - fd : int + - wds : dict[int, str] + - _thread : Thread | None + - _stop_event : Event + -- + + start() : None + + stop() : None + - _watch_loop() : None + } + + BaseFileWatcher <|-- InotifyFileWatcher +} + +'───────────────────────────────────────── +' Workflow (workflow.py) +'───────────────────────────────────────── +class "workflow.py" as WorkflowModule <> { + + resolve_prefix(agent, prefix, project_root) : str + + get_agent_workflow(pfx) : str +} + +'───────────────────────────────────────── +' Cross-component relationships +'───────────────────────────────────────── +CLI ..> LibspecRepl : "launches" +CLI ..> FastMCPServer : "starts" +CLI ..> AgentConfig : "calls configure()" +CLI ..> WorkflowModule : "calls" + +LibspecRepl ..> InotifyFileWatcher : "uses" +FastMCPServer ..> AgentConfig : "calls configure()" +FastMCPServer ..> WorkflowModule : "calls" + +@enduml diff --git a/docs/analysis/class_diagram_infrastructure.svg b/docs/analysis/class_diagram_infrastructure.svg new file mode 100644 index 0000000..1dd4ce6 --- /dev/null +++ b/docs/analysis/class_diagram_infrastructure.svg @@ -0,0 +1 @@ +libspec — Infrastructure Classes (CLI, REPL, MCP, AgentConfig, Watcher)libspec — Infrastructure Classes (CLI, REPL, MCP, AgentConfig, Watcher)CLI (clipy)REPL (replpy)MCP Server (mcp_serverpy)AgentConfig (agent_configpy)Watcher (watcherpy)main (click group)+init()+diff(commit_a, commit_b)+mcp()+mcp_agent(agent, project_root, list_agents)+agent_config(agent, project_root, list_agents)+repl()+list(commit_ref)+show(component_ref, commit_ref)+search(query, commit_ref)+log()+dependencies(commit_ref)+agent_workflow_cmd(agent, prefix)+completion_cmd(shell)Click-based entry point.Registered as "libspec" script.ReplCommand+name() : str [abstract]+desc() : str [abstract]+run(repl, arg) : bool [abstract]+usage() : strHelpCommandListCommandShowCommandSearchCommandDiffCommandLogCommandDependenciesCommandQuitCommandReplCommander+commands : dict[str, ReplCommand]+register(cmd) : None+dispatch(repl, line) : boolLibspecRepl+components : list[Component]+active_session_id : str | None+commander : ReplCommander-_watcher : InotifyFileWatcher | None+start() : None+load_components() : None+active_session_str() : str-_build_prompt_session() : PromptSession-_setup_watcher() : None-_handle_input(text) : bool«FastMCP»FastMCPServer+diff(commit_a, commit_b) : str+search(query) : str+peek(file_path, line, character) : str+usage(file_path, line, character) : str+symbols(file_path) : str+mcp_agent(agent, project_root, list_agents) : str+agent_config(agent, project_root, list_agents) : str+list_components(commit) : str+show_component(component_ref, commit) : str+list_dependencies(commit) : str+agent_workflow(agent, prefix) : str+main() : NoneRegistered as "libspec-mcp" script.Transport: stdio (MCP protocol).AgentConfig+project_root : str+uv_path : str+mcp_command : dict+_registry : dict[str, type]+configure() : str [abstract]+agent_id : str [abstract]+agent_display_name : str [abstract]+agent_description : str [abstract]+skill_dir_path : str+is_active : bool+is_skill_up_to_date() : bool-_render_skill() : str-_install_skill(dir_path, content) : None-_backup_if_exists(config_path) : None-_load_json_config(path) : dict-_save_json_config(path, config) : None-_load_toml_config(path) : dict-_save_toml_config(path, config) : NoneAntigravityConfigagent_id = "antigravity"GeminiConfigagent_id = "gemini"ClaudeConfigagent_id = "claude"CopilotConfigagent_id = "copilot"CodexConfigagent_id = "codex"OpenCodeConfigagent_id = "opencode"BaseFileWatcher+paths : list[str]+on_change : Callable+start() : None [abstract]+stop() : None [abstract]InotifyFileWatcher-fd : int-wds : dict[int, str]-_thread : Thread | None-_stop_event : Event+start() : None+stop() : None-_watch_loop() : None«module»workflow.py+resolve_prefix(agent, prefix, project_root) : str+get_agent_workflow(pfx) : strlaunchesstartscalls configure()callsusescalls configure()calls \ No newline at end of file diff --git a/docs/analysis/class_diagram_spec_types.puml b/docs/analysis/class_diagram_spec_types.puml new file mode 100644 index 0000000..c4979ee --- /dev/null +++ b/docs/analysis/class_diagram_spec_types.puml @@ -0,0 +1,140 @@ +@startuml class_diagram_spec_types +title libspec — Built-in Specification Vocabulary (spec_types.py) + +skinparam classAttributeIconSize 0 +skinparam monochrome false +skinparam shadowing false +skinparam backgroundColor #FAFAFA +skinparam class { + BackgroundColor #F0FDF4 + BorderColor #16A34A + ArrowColor #16A34A + FontName Helvetica +} +skinparam note { + BackgroundColor #FFFDE7 + BorderColor #F59E0B +} + +'───────────────────────────────────────── +' Root +'───────────────────────────────────────── +abstract class Ctx <<(C,#EEF2FF)core>> { +} + +'───────────────────────────────────────── +' Mixin +'───────────────────────────────────────── +class LeafMethods <> { + + methods() : list[dict] + - _leaf_public_functions() : Iterable + - _method_descriptor(name, func) : dict + - _method_params_without_self(func) : list[str] + - _invoke_member_for_preview(member, arg_count) : Any +} + +'───────────────────────────────────────── +' Vocabulary types +'───────────────────────────────────────── +class Feature { + + feature_name() : str + + date() : str [abstract] + + description() : str [abstract] +} +note right of Feature : Template:\n"Feature Specification: {{feature_name}}" + +class Def { + + name() : str +} +note right of Def : Template:\n"Definition: {{name}}:" + +class EdgeCase { + + boundary_condition() : str [abstract] + + error_scenario() : str [abstract] +} + +class Constraint { + + constraint_id() : str + + description() : str +} +note right of Constraint : CONSTRAINT-ID / DESCRIPTION /\nENFORCEMENT template + +class Requirement { + + title() : str + + req_id() : str +} +note right of Requirement : "TITLE / REQUIREMENT-ID template.\nEmbed REQUIREMENT-ID in source\nfor cross-reference." + +class SystemRequirement { +} +note right of SystemRequirement : "Tool-level (toolchain) requirement" + +class DataSchema { + + model_name() : str + + fields() : dict +} +note right of DataSchema : "DATA-MODEL / FIELDS Jinja2 template" + +class SQLite3 { + + dbpath() : str [abstract] +} + +class PeeWee { + + dbpath() : str [abstract] +} + +class API { + + api_name() : str + + constraints() : list[str] +} +note right of API : "API Specification template with\nendpoints list via LeafMethods" + +class LibraryAPI { + + version() : str [abstract] +} + +class RestMixin <> { +} +note right of RestMixin : "Guidance mixin: 'Develop a REST API\nwith best practices'" + +class CmdLine { +} +note right of CmdLine : "Command Line Specification template\nwith methods list via LeafMethods" + +class Implementation { + + implementation_directory() : str [abstract] +} + +class UserStory { + + brief_title() : str [abstract] + + priority() : str [abstract] + + user_journey() : str [abstract] + + explanation() : str [abstract] + + acceptance_scenarios() : str [abstract] +} + +'───────────────────────────────────────── +' Inheritance +'───────────────────────────────────────── +Ctx <|-- Feature +Ctx <|-- Def +Ctx <|-- EdgeCase +Ctx <|-- Constraint +Ctx <|-- Requirement +Ctx <|-- DataSchema +Ctx <|-- API +Ctx <|-- CmdLine + +Requirement <|-- SystemRequirement +Requirement <|-- Implementation +Feature <|-- UserStory + +DataSchema <|-- SQLite3 +DataSchema <|-- PeeWee + +API <|-- LibraryAPI + +API ..|> LeafMethods : "uses" +CmdLine ..|> LeafMethods : "uses" + +@enduml diff --git a/docs/analysis/class_diagram_spec_types.svg b/docs/analysis/class_diagram_spec_types.svg new file mode 100644 index 0000000..3f7fa22 --- /dev/null +++ b/docs/analysis/class_diagram_spec_types.svg @@ -0,0 +1 @@ +libspec — Built-in Specification Vocabulary (spec_types.py)libspec — Built-in Specification Vocabulary (spec_types.py)«core»Ctx«mixin»LeafMethods+methods() : list[dict]-_leaf_public_functions() : Iterable-_method_descriptor(name, func) : dict-_method_params_without_self(func) : list[str]-_invoke_member_for_preview(member, arg_count) : AnyFeature+feature_name() : str+date() : str [abstract]+description() : str [abstract]Template:"Feature Specification: {{feature_name}}"Def+name() : strTemplate:"Definition: {{name}}:"EdgeCase+boundary_condition() : str [abstract]+error_scenario() : str [abstract]Constraint+constraint_id() : str+description() : strCONSTRAINT-ID / DESCRIPTION /ENFORCEMENT templateRequirement+title() : str+req_id() : str"TITLE / REQUIREMENT-ID template.Embed REQUIREMENT-ID in sourcefor cross-reference."SystemRequirement"Tool-level (toolchain) requirement"DataSchema+model_name() : str+fields() : dict"DATA-MODEL / FIELDS Jinja2 template"SQLite3+dbpath() : str [abstract]PeeWee+dbpath() : str [abstract]API+api_name() : str+constraints() : list[str]"API Specification template withendpoints list via LeafMethods"LibraryAPI+version() : str [abstract]«mixin»RestMixin"Guidance mixin: 'Develop a REST APIwith best practices'"CmdLine"Command Line Specification templatewith methods list via LeafMethods"Implementation+implementation_directory() : str [abstract]UserStory+brief_title() : str [abstract]+priority() : str [abstract]+user_journey() : str [abstract]+explanation() : str [abstract]+acceptance_scenarios() : str [abstract]usesuses \ No newline at end of file diff --git a/docs/analysis/overview.md b/docs/analysis/overview.md new file mode 100644 index 0000000..63a078d --- /dev/null +++ b/docs/analysis/overview.md @@ -0,0 +1,219 @@ +# libspec — Architectural Overview + +> **Version**: 10.3.2 +> **Language**: Python 3.12+ +> **License**: GPL-3.0-or-later + +--- + +## 1. What is libspec? + +`libspec` is a **spec-driven development library** designed to help software teams (and LLM coding agents) work with structured, versioned software specifications. Specifications are written as ordinary Python class hierarchies, compiled into structured data (`Component` objects) and optionally into XML, and then used to drive the implementation lifecycle. + +The core insight is: **specifications are code**. They live alongside source code in version control, are diffable at the requirement level, and can be consumed by AI coding agents through a standard MCP (Model Context Protocol) interface. + +--- + +## 2. Repository Structure + +``` +libspec/ +├── libspec/ # Main Python package (the library) +│ ├── spec.py # Core engine: Spec (orchestrator) + Ctx (spec base class) +│ ├── spec_types.py # Built-in vocabulary: Feature, Requirement, API, DataSchema, … +│ ├── common.py # Frozen dataclasses: Component, Snapshot, Implemented +│ ├── store.py # Store error types (SpecStore has been removed — Git-native only) +│ ├── util.py # compile_live_spec, compile_git_spec, find_implementations_in_workspace +│ ├── spec_diff.py # Native specification diff engine (Component-level) +│ ├── cli.py # click-based CLI (init, diff, list, show, search, mcp, repl, …) +│ ├── repl.py # Interactive specification inspector REPL (prompt_toolkit) +│ ├── mcp_server.py # FastMCP server exposing spec tools to AI agents +│ ├── agent_config.py # Configures AI agent integrations (Claude, Gemini, Copilot, etc.) +│ ├── workflow.py # Returns the standardised 8-step developer agent workflow +│ ├── watcher.py # Linux inotify-based file-change watcher (used by REPL) +│ ├── colors.py # Terminal color/theme constants +│ └── err.py # UnimplementedMethodError +├── spec/ # libspec's own self-specification (dogfooding) +│ ├── main_spec.py # Root MainSpec(Spec) listing all spec modules +│ ├── app.py # Top-level requirements +│ ├── core.py # Specs for Ctx / Spec engine +│ ├── types.py # Specs for built-in vocabulary +│ ├── err.py # Error-handling specs (Req / Feat base classes) +│ └── … +└── tests/ # pytest test suite +``` + +--- + +## 3. Core Concepts + +### 3.1 Specifications as Python Classes + +A *specification* is a Python class that: + +1. Inherits (directly or transitively) from `Ctx`. +2. Has a **docstring** — either plain text or a **Jinja2 template**. +3. May define **methods** that supply values for template variables. +4. May inherit from other `Ctx` subclasses to form a requirement hierarchy. + +```python +# Example project specification (lives in spec/app.py) +from libspec import Requirement, Feature + +class MustBeFast(Requirement): + """ + TITLE: Performance Requirement + REQUIREMENT-ID: spec.app.MustBeFast + The system MUST respond to any user request within {{max_ms}} ms. + """ + def max_ms(self): + return 200 + +class UserDashboard(Feature, MustBeFast): + """The user dashboard must display a real-time feed.""" +``` + +### 3.2 Spec (Orchestrator) + +`Spec` is the top-level orchestrator. Users subclass it and implement `modules()`: + +```python +class MainSpec(Spec): + def modules(self): + return [app, core, types] +``` + +`Spec` walks all modules, instantiates every `Ctx` subclass, and either: +- **`get_components()`** — returns a flat list of `Component` frozen dataclasses. +- **`generate_xml()`** / **`write_xml()`** — serialises to a structured XML document. + +### 3.3 Component (Data Artefact) + +Each compiled spec class produces a `Component`: + +| Field | Description | +|-------|-------------| +| `ref` | Fully-qualified class name (`spec.app.MustBeFast`) | +| `docstring` | Rendered (Jinja2) specification text | +| `is_template` | True if the docstring contained `{{ }}` or `{% %}` | +| `inherits` | List of FQNs of parent Ctx classes with docstrings | +| `hash` | SHA-256 of the rendered docstring (content fingerprint) | +| `is_dependency` | True for inherited classes not defined in the project's own modules | + +### 3.4 Git-Native Versioning + +libspec has **no database**. Versioning relies entirely on Git: + +- `compile_live_spec()` — imports the `spec/` directory on disk. +- `compile_git_spec(ref)` — extracts `spec/` from a Git archive at `ref` into a tempdir and compiles it. +- Results are cached in `.libspec/cache/` using a SHA-256 fingerprint to avoid redundant recompilation. + +### 3.5 Native Spec Diff + +`spec_diff.generate_native_patch()` compares two sets of `Component` lists: + +- **NEW**: component present in new but not old. +- **REMOVED**: component present in old but not new. +- **CHANGED**: hash differs; performs recursive inheritance diff. + +Changes are printed as a human-readable unified-diff-style patch at the requirement level. + +--- + +## 4. Entry Points + +### 4.1 CLI (`libspec`) + +Implemented with `click`. Key commands: + +| Command | Purpose | +|---------|---------| +| `libspec init` | Scaffold a new `spec/` directory with boilerplate | +| `libspec diff [A] [B]` | Native spec diff (defaults to HEAD vs live) | +| `libspec list [-c ref]` | List all components (live or at a Git ref) | +| `libspec show ` | Inspect a single component and its implementation claims | +| `libspec search ` | Full-text search across refs and docstrings | +| `libspec log` | Git commit history of the `spec/` directory | +| `libspec dependencies` | Show component inheritance graph | +| `libspec repl` | Launch the interactive specification REPL | +| `libspec mcp` | Start the MCP server (stdio transport) | +| `libspec agent-config ` | Install MCP + skill for an AI agent | +| `libspec agent-workflow` | Print the standardised 8-step workflow | +| `libspec completion ` | Generate shell auto-completion script | + +### 4.2 MCP Server (`libspec-mcp`) + +The MCP server exposes libspec tools to AI coding agents: + +| Tool | Purpose | +|------|---------| +| `diff` | Spec diff between two Git refs | +| `search` | AST-based workspace search | +| `peek` | Hover / definition lookup at file position | +| `usage` | Find references to a component | +| `symbols` | List classes/methods in a file | +| `list_components` | List all spec components | +| `show_component` | Inspect a single component | +| `list_dependencies` | Show component dependency graph | +| `agent_workflow` | Return the 8-step workflow instructions | +| `agent_config` / `mcp_agent` | Configure coding agents | + +### 4.3 REPL + +An interactive `prompt_toolkit`-based REPL for browsing specifications: + +- **Commands**: `list`, `show`, `search`, `diff`, `log`, `dependencies`, `help`, `quit` +- Auto-completion of component refs +- Linux inotify watcher auto-reloads specs on file change +- Snapshot navigation (browse specs at historical Git commits) + +--- + +## 5. Agent Integration + +`AgentConfig` is an ABC with concrete subclasses for each supported coding agent: + +| Agent ID | Tool | Config Location | +|----------|------|----------------| +| `claude` | Claude Code | `.claude/` | +| `gemini` | Gemini CLI | `.gemini/settings.json` | +| `antigravity` | Antigravity IDE | `.gemini/antigravity/mcp_config.json` | +| `copilot` | GitHub Copilot | `.github/mcp.json` | +| `codex` | OpenAI Codex | `.codex/config.toml` | +| `opencode` | OpenCode | `.opencode/opencode.json` | + +Each configurator: +1. Writes the agent's MCP server config pointing to `uv run libspec mcp`. +2. Renders and installs a `SKILL.md` (validated via `skillkit`) containing the 8-step developer workflow. +3. Auto-heals outdated skills on `libspec init`, `libspec mcp`, and `libspec agent-config`. + +--- + +## 6. Developer Agent Workflow + +The canonical 8-step workflow (surfaced via `agent_workflow`): + +1. **Edit Spec** — Decompose requirements into granular, single-responsibility `Ctx` subclasses. +2. **Diff Spec** *(mandatory before coding)* — Run `libspec diff` to detect specification drift. +3. **Sort Implementation Order** — Use `libspec dependencies` to build a topological ordering. +4. **TDD** — Write tests first for each component in dependency order. +5. **Implement** — Write code to make the tests pass. +6. **Code Quality** — Lint, format, and static analysis. +7. **Verify Spec Sync** — Run `libspec diff` again to confirm everything is accounted for. +8. **Author a git commit message** — Present to the user. + +--- + +## 7. Key Design Decisions + +| Decision | Rationale | +|----------|-----------| +| Specs are Python classes | Native IDE support, import system, inheritance, type checking | +| Jinja2 docstring templates | Allows reusable parameterised requirement patterns | +| Git-native versioning (no DB) | Zero infrastructure, works anywhere Git works | +| SHA-256 content hashing | Deterministic change detection without timestamps | +| Component-level diff (not XML diff) | Understands inheritance; produces requirement-level patches | +| MCP for AI integration | Standard protocol; works with Claude, Gemini, Copilot, etc. | +| `SKILL.md` skill injection | Ensures AI agents know the workflow without manual setup | +| inotify watcher in REPL | Live spec reload without polling | +| `.libspec/` marker dir | Simple project detection, consistent with `.git/` convention |