woods 1.6.1 → 2.0.0.beta2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# MCP Registration in Git Worktrees
|
|
2
|
+
|
|
3
|
+
> **Claude Code specific.** This page covers Claude Code's MCP registration model (`/mcp`, `~/.claude/plugins/`); other MCP clients manage per-directory registration their own way.
|
|
4
|
+
|
|
5
|
+
When you work in a git worktree, a separate directory checked out from the same repository, your MCP tools may not be available to subagents running in that directory. This page explains why, how to fix it, and how to confirm the registration took effect.
|
|
6
|
+
|
|
7
|
+
## Why Worktree Subagents May Not See Woods Tools
|
|
8
|
+
|
|
9
|
+
MCP server registration in Claude Code is controlled by `.mcp.json` files. Claude Code discovers these files by walking up the directory tree from the working directory. It stops at the first `.mcp.json` it finds (or at `~/.claude/settings.json` for global registrations).
|
|
10
|
+
|
|
11
|
+
A git worktree has its own root directory separate from the main repository checkout. When a subagent starts inside the worktree root, it walks up from that path, not from the main repository root. Unless a `.mcp.json` exists inside the worktree directory tree or in an ancestor shared with both checkouts, the subagent sees no MCP servers.
|
|
12
|
+
|
|
13
|
+
Example directory layout:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
~/work/my-app/ ← main checkout, has .mcp.json here
|
|
17
|
+
~/work/my-app-feature/ ← worktree, no .mcp.json, so MCP tools are missing
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A subagent spawned in `~/work/my-app-feature/` will not find the `.mcp.json` from `~/work/my-app/`.
|
|
21
|
+
|
|
22
|
+
## Fix: Add a `.mcp.json` to the Worktree Root
|
|
23
|
+
|
|
24
|
+
Create a `.mcp.json` in the worktree's root directory with the same woods server entries you use in the main checkout:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"woods": {
|
|
30
|
+
"command": "woods-mcp-start",
|
|
31
|
+
"args": ["./tmp/woods"]
|
|
32
|
+
},
|
|
33
|
+
"woods-console": {
|
|
34
|
+
"command": "docker",
|
|
35
|
+
"args": [
|
|
36
|
+
"compose", "exec", "-T", "app",
|
|
37
|
+
"bundle", "exec", "rake", "woods:console"
|
|
38
|
+
],
|
|
39
|
+
"cwd": "/absolute/host/path/to/worktree"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Adjust the paths and arguments to match your project's setup. In particular:
|
|
46
|
+
|
|
47
|
+
- `./tmp/woods` is a relative path, it resolves against the worktree root, which is correct if extraction output is written into each worktree separately.
|
|
48
|
+
- If you share a single extraction output directory between checkouts, use the absolute path to the shared output: `"/absolute/path/to/main-checkout/tmp/woods"`.
|
|
49
|
+
- For Docker projects, the `docker compose exec` command works the same from any host path.
|
|
50
|
+
|
|
51
|
+
## How Plugin Discovery Works
|
|
52
|
+
|
|
53
|
+
Claude Code also discovers MCP servers registered in plugin manifests. A plugin at `~/.claude/plugins/<plugin-name>/admin-tools/.mcp.json` is loaded globally, its servers are available in any session regardless of working directory.
|
|
54
|
+
|
|
55
|
+
If your team distributes the woods MCP registration through a shared plugin, the worktree problem does not apply. Check whether woods is already registered this way:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ls ~/.claude/plugins/
|
|
59
|
+
# look for a directory containing admin-tools/.mcp.json
|
|
60
|
+
cat ~/.claude/plugins/<plugin-name>/admin-tools/.mcp.json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If you find the woods servers registered there, subagents in any worktree will have access automatically, you do not need a per-worktree `.mcp.json`.
|
|
64
|
+
|
|
65
|
+
## Verifying MCP Registration for a Subagent
|
|
66
|
+
|
|
67
|
+
### Option 1: List tools from a claude session in the worktree
|
|
68
|
+
|
|
69
|
+
Open a new Claude Code session with the worktree as the working directory and run:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
/mcp
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This lists all connected MCP servers and their tools. If `woods` and/or `woods-console` appear, registration is working.
|
|
76
|
+
|
|
77
|
+
### Option 2: Check via the woods status tool
|
|
78
|
+
|
|
79
|
+
Ask Claude to call the status tool:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Use woods-console console_status to check what models are available.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If the tool runs successfully, MCP is registered and the console server is reachable.
|
|
86
|
+
|
|
87
|
+
### Option 3: Inspect the MCP config that Claude Code loaded
|
|
88
|
+
|
|
89
|
+
From the worktree directory, run:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cat .mcp.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If the file exists and contains the woods entries, Claude Code will use it. If the file is missing, check parent directories up to your home directory for any `.mcp.json` that would be discovered.
|
|
96
|
+
|
|
97
|
+
## Extraction Provenance in Worktrees (`git_branch` / `git_sha`)
|
|
98
|
+
|
|
99
|
+
`manifest.json` records the `git_branch` and `git_sha` the extraction ran against. In a linked worktree, `.git` is a **file** containing a `gitdir:` pointer to the real git directory, frequently an absolute host path, rather than a `.git` directory.
|
|
100
|
+
|
|
101
|
+
Woods resolves provenance with worktree-aware git plumbing (`git -C <root> rev-parse`), so an ordinary worktree reports the correct branch and SHA. When a `.git` is present but the pointed-to git directory **cannot be resolved**: most commonly a worktree extracted inside a container where the host path isn't mounted. Woods records `git_branch: "unknown"` / `git_sha: "unknown"` rather than a stale, misleading value: a baked `GIT_BRANCH`/`GIT_SHA` build arg is **not** trusted here (it could be stale). The env vars are honored only when there is no `.git` at the root at all (a non-repo checkout, e.g. a Docker `COPY` that excludes `.git`) or git is unavailable.
|
|
102
|
+
|
|
103
|
+
To get correct provenance from a containerized worktree, mount the directory the `gitdir:` pointer references (the parent repository's `.git`) into the container so git can resolve it. Temporal snapshots skip an `"unknown"` SHA, so misleading provenance never keys a snapshot.
|
|
104
|
+
|
|
105
|
+
## Troubleshooting
|
|
106
|
+
|
|
107
|
+
**"Unknown tool" or "no MCP server named woods"**
|
|
108
|
+
|
|
109
|
+
The woods MCP server is not registered for this session. Add a `.mcp.json` to the worktree root as shown above, then restart the session.
|
|
110
|
+
|
|
111
|
+
**Console server starts but returns "unsupported: Not yet implemented in embedded mode" for console_sql / console_query**
|
|
112
|
+
|
|
113
|
+
The console server is registered and reachable, but `embedded_read_tools` is disabled (the default). To enable console_sql and console_query in embedded mode, see the [Console MCP Setup guide](CONSOLE_MCP_SETUP.md), specifically the `embedded_read_tools: true` option for the Rack middleware.
|
|
114
|
+
|
|
115
|
+
**Extraction output is empty or stale in the worktree**
|
|
116
|
+
|
|
117
|
+
Extraction writes to `tmp/woods/` relative to the Rails application root (inside the container). If the worktree's volume mount points to a different host path than the main checkout, run extraction again from the worktree:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
docker compose exec app bundle exec rake woods:extract
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
See [DOCKER_SETUP.md](DOCKER_SETUP.md) for the full Docker workflow.
|
|
124
|
+
|
|
125
|
+
**Worktree `.mcp.json` conflicts with main checkout `.mcp.json`**
|
|
126
|
+
|
|
127
|
+
Each file is independent. Claude Code loads the one closest to the working directory. There is no inheritance or merging between them. Keep both files in sync manually, or move the shared configuration into a global plugin manifest.
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Notion Integration
|
|
2
|
+
|
|
3
|
+
Sync your Rails application's data model documentation to Notion databases, making schema, associations, validations, callbacks, and column-level detail accessible to non-technical stakeholders without GitHub access.
|
|
4
|
+
|
|
5
|
+
## What Gets Synced
|
|
6
|
+
|
|
7
|
+
Woods extracts your Rails application via runtime introspection and pushes structured data to two Notion databases:
|
|
8
|
+
|
|
9
|
+
**Data Models Database**: One page per ActiveRecord model:
|
|
10
|
+
- Table name, model class name, file path
|
|
11
|
+
- Associations (has_many, belongs_to, has_one, through, polymorphic)
|
|
12
|
+
- Validations (grouped by attribute)
|
|
13
|
+
- Callbacks with side-effect analysis (jobs enqueued, services called)
|
|
14
|
+
- Scopes
|
|
15
|
+
- Column count
|
|
16
|
+
- Git metadata (last modified date, change frequency)
|
|
17
|
+
- Dependencies (services, jobs, other models referenced)
|
|
18
|
+
- Last schema change (from migration history)
|
|
19
|
+
|
|
20
|
+
**Columns Database**: One page per database column:
|
|
21
|
+
- Column name, data type, nullable, default value
|
|
22
|
+
- Validation rules (matched from model-level validations)
|
|
23
|
+
- Relation to parent Data Models page
|
|
24
|
+
|
|
25
|
+
All syncs are **idempotent**: existing pages are updated, new pages are created. Re-running the sync is always safe.
|
|
26
|
+
|
|
27
|
+
## Sync Manifest (Incremental Sync)
|
|
28
|
+
|
|
29
|
+
Sync is incremental. A **sync manifest** (`<output_dir>/notion_sync_manifest.json`) records the content hash and Notion page id of every page last pushed. On each run the exporter:
|
|
30
|
+
|
|
31
|
+
- **skips** a page whose built content hash is unchanged (zero API calls),
|
|
32
|
+
- **updates** only pages whose content actually changed, via the cached page id,
|
|
33
|
+
- **self-heals** the cached id if the page was deleted or archived behind it (one lookup, then a normal create).
|
|
34
|
+
|
|
35
|
+
Manifest entries for models/columns that vanished from the current extraction are pruned, but **no Notion page is ever deleted** by the sync, there is no delete path. A renamed or removed model just leaves its old page in Notion untouched.
|
|
36
|
+
|
|
37
|
+
If the manifest is missing (first run, or a CI cache miss), the exporter falls back to the full lookup/create path for every page and rebuilds the manifest, correct, just more API calls than a steady-state run.
|
|
38
|
+
|
|
39
|
+
### Escape hatch
|
|
40
|
+
|
|
41
|
+
Set `WOODS_NOTION_FORCE=1` (or pass `force_full: true` to the exporter) to ignore the manifest for one run and re-check every page, useful after a mapper/format change that alters every page's content, where the unchanged-hash skip would otherwise mask the update.
|
|
42
|
+
|
|
43
|
+
## Setup
|
|
44
|
+
|
|
45
|
+
### 1. Create a Notion Integration
|
|
46
|
+
|
|
47
|
+
1. Go to [notion.so/my-integrations](https://www.notion.so/my-integrations)
|
|
48
|
+
2. Create a new internal integration
|
|
49
|
+
3. Copy the API token (starts with `secret_`)
|
|
50
|
+
|
|
51
|
+
### 2. Create Notion Databases
|
|
52
|
+
|
|
53
|
+
Create two databases in your Notion workspace with these exact property names and types:
|
|
54
|
+
|
|
55
|
+
**Data Models Database:**
|
|
56
|
+
|
|
57
|
+
| Property | Type |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Table Name | Title |
|
|
60
|
+
| Model Name | Text |
|
|
61
|
+
| Description | Text |
|
|
62
|
+
| Associations | Text |
|
|
63
|
+
| Validations | Text |
|
|
64
|
+
| Callbacks | Text |
|
|
65
|
+
| Scopes | Text |
|
|
66
|
+
| Column Count | Number |
|
|
67
|
+
| Last Modified | Date |
|
|
68
|
+
| Change Frequency | Select (options: new, hot, active, stable, dormant) |
|
|
69
|
+
| Last Schema Change | Date |
|
|
70
|
+
| File Path | Text |
|
|
71
|
+
| Dependencies | Text |
|
|
72
|
+
|
|
73
|
+
**Columns Database:**
|
|
74
|
+
|
|
75
|
+
| Property | Type |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Column Name | Title |
|
|
78
|
+
| Table | Relation (→ Data Models database) |
|
|
79
|
+
| Data Type | Select (options: string, integer, bigint, boolean, datetime, text, decimal, float, date, binary, json, jsonb) |
|
|
80
|
+
| Nullable | Checkbox |
|
|
81
|
+
| Default Value | Text |
|
|
82
|
+
| Validation Rules | Text |
|
|
83
|
+
|
|
84
|
+
### 3. Share Databases with Integration
|
|
85
|
+
|
|
86
|
+
Open each database in Notion, click the `...` menu → "Connections" → add your integration.
|
|
87
|
+
|
|
88
|
+
### 4. Get Database IDs
|
|
89
|
+
|
|
90
|
+
Each database URL contains its ID: `https://notion.so/{workspace}/{database_id}?v=...`
|
|
91
|
+
|
|
92
|
+
### 5. Configure Woods
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# config/initializers/woods.rb
|
|
96
|
+
Woods.configure do |config|
|
|
97
|
+
config.notion_api_token = Rails.application.credentials.dig(:notion, :api_token)
|
|
98
|
+
config.notion_database_ids = {
|
|
99
|
+
data_models: 'your-data-models-database-id',
|
|
100
|
+
columns: 'your-columns-database-id'
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Or via environment variables:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
export NOTION_API_TOKEN=secret_...
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Common Workflows
|
|
112
|
+
|
|
113
|
+
### Full Extraction + Sync
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Extract everything from the Rails app, then push to Notion
|
|
117
|
+
bundle exec rake woods:extract
|
|
118
|
+
bundle exec rake woods:notion_sync
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Post-Migration Sync
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Re-extract changed files after a migration, then sync
|
|
125
|
+
bundle exec rake woods:incremental
|
|
126
|
+
bundle exec rake woods:notion_sync
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Buildkite CI Integration
|
|
130
|
+
|
|
131
|
+
Add to your `pipeline.yml`:
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
steps:
|
|
135
|
+
- label: ":database: Extract & Sync to Notion"
|
|
136
|
+
command: |
|
|
137
|
+
bundle exec rake woods:extract
|
|
138
|
+
bundle exec rake woods:notion_sync
|
|
139
|
+
if: build.branch == "main" && build.state == "passed"
|
|
140
|
+
soft_fail: true
|
|
141
|
+
env:
|
|
142
|
+
NOTION_API_TOKEN: $NOTION_API_TOKEN # Set in pipeline environment settings
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For post-migration syncs only:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
steps:
|
|
149
|
+
- label: ":database: Sync Schema to Notion"
|
|
150
|
+
command: |
|
|
151
|
+
bundle exec rake woods:incremental
|
|
152
|
+
bundle exec rake woods:notion_sync
|
|
153
|
+
if: |
|
|
154
|
+
build.message =~ /migrate/i ||
|
|
155
|
+
build.env("FORCE_SCHEMA_SYNC") == "true"
|
|
156
|
+
soft_fail: true
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### MCP Server
|
|
160
|
+
|
|
161
|
+
If using the MCP Index Server, the `notion_sync` tool is available:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"tool": "notion_sync",
|
|
166
|
+
"arguments": {}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Selective Sync
|
|
171
|
+
|
|
172
|
+
Only sync Data Models (skip Columns):
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
Woods.configure do |config|
|
|
176
|
+
config.notion_database_ids = { data_models: 'db-uuid' }
|
|
177
|
+
# columns key omitted → column sync is skipped
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## What the Gem Handles vs. the Host App
|
|
182
|
+
|
|
183
|
+
### Gem (automated)
|
|
184
|
+
|
|
185
|
+
| Database | Content | Source |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| Data Models | Schema, associations, validations, callbacks, scopes, git metadata | ModelExtractor + MigrationExtractor |
|
|
188
|
+
| Columns | Column details, types, defaults, validation rules | ModelExtractor metadata |
|
|
189
|
+
|
|
190
|
+
### Host App (you build these)
|
|
191
|
+
|
|
192
|
+
| Database | Content | How to Build |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| Features | Feature ownership, status, user stories, acceptance criteria | Human-authored in Notion |
|
|
195
|
+
| User Flows | User-facing flow documentation, triggers, endpoints | Human-authored (future: gem can auto-populate from controller/route extraction) |
|
|
196
|
+
| Data Handling | PII classification, encryption, retention policies | Human policy decisions |
|
|
197
|
+
| Deploys | Build numbers, environments, deployers, commit SHAs | Buildkite webhook → Rails controller → Notion API |
|
|
198
|
+
|
|
199
|
+
### Example: Deploy Tracking (Host App)
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# app/controllers/webhooks/buildkite_controller.rb
|
|
203
|
+
module Webhooks
|
|
204
|
+
class BuildkiteController < ApplicationController
|
|
205
|
+
skip_before_action :verify_authenticity_token
|
|
206
|
+
before_action :verify_buildkite_token
|
|
207
|
+
|
|
208
|
+
def create
|
|
209
|
+
payload = JSON.parse(request.body.read)
|
|
210
|
+
NotionDeploySyncJob.perform_later(payload)
|
|
211
|
+
head :ok
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# app/jobs/notion_deploy_sync_job.rb
|
|
217
|
+
class NotionDeploySyncJob < ApplicationJob
|
|
218
|
+
def perform(payload)
|
|
219
|
+
build = payload["build"]
|
|
220
|
+
# Use your own NotionClient to create a Deploys page
|
|
221
|
+
# This is outside Woods's scope
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Rate Limiting
|
|
227
|
+
|
|
228
|
+
Notion's API allows 3 requests per second. The gem's built-in rate limiter handles this automatically. For large codebases (100+ models), expect the sync to take a few minutes.
|
|
229
|
+
|
|
230
|
+
If you see 429 errors, the client retries up to 3 times with exponential backoff using the `Retry-After` header.
|
|
231
|
+
|
|
232
|
+
### Retries and duplicates
|
|
233
|
+
|
|
234
|
+
A 429 is always retried, for any request, the server rejected it before doing any work. A 503 is different: an intermediary in front of Notion's API can synthesize a 503 for a request the origin already committed, so blindly retrying it risks creating a duplicate page. The client only retries a 503 for **idempotent** requests (reads, and page updates keyed by an existing page id). Page *creation* has no idempotency key, so a 503 on `create_page` is raised immediately instead of retried, you'll see the error and can re-run the sync, which is safe because the manifest either already recorded the created page or will create it fresh.
|
|
235
|
+
|
|
236
|
+
## Error Handling
|
|
237
|
+
|
|
238
|
+
The sync collects errors per-model and per-column without stopping. The stats hash returned by `sync_all` includes an `errors` array:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
stats = exporter.sync_all
|
|
242
|
+
stats[:errors] # => ["User: Notion API error 400: ...", ...]
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Common errors:
|
|
246
|
+
- **401 Unauthorized**: Check your API token
|
|
247
|
+
- **404 Not Found**: Check database IDs and ensure the integration has access
|
|
248
|
+
- **400 Validation Error**: Check that Notion database properties match the expected schema above
|
|
249
|
+
- **429 Rate Limited**: Automatic retry (up to 3 times)
|
|
250
|
+
|
|
251
|
+
`rake woods:notion_sync` prints the first five errors and then **exits 1**, matching `woods:unblocked_sync` and `woods:obsidian`. A printed-but-green run is invisible in a post-merge pipeline: a dead token would otherwise keep the job green while the Notion database went stale indefinitely. Whatever synced before the failure is durable, so re-running after the fix converges. The Buildkite examples above use `soft_fail: true`; drop it if you want the sync failure to fail the build.
|
|
252
|
+
|
|
253
|
+
## Architecture
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
Extraction Output (JSON on disk)
|
|
257
|
+
↓
|
|
258
|
+
┌─────────────────────┐
|
|
259
|
+
│ IndexReader │ ← Reads model, column, migration data
|
|
260
|
+
└─────────────────────┘
|
|
261
|
+
↓
|
|
262
|
+
┌─────────────────────┐
|
|
263
|
+
│ Exporter │ ← Orchestrates sync flow
|
|
264
|
+
├─────────────────────┤
|
|
265
|
+
│ ModelMapper │ ← Maps ExtractedUnit → Notion Data Models properties
|
|
266
|
+
│ ColumnMapper │ ← Maps column metadata → Notion Columns properties
|
|
267
|
+
│ MigrationMapper │ ← Extracts latest migration dates per table
|
|
268
|
+
└─────────────────────┘
|
|
269
|
+
↓
|
|
270
|
+
┌─────────────────────┐
|
|
271
|
+
│ Client │ ← Notion API wrapper (Net::HTTP, rate-limited)
|
|
272
|
+
│ RateLimiter │ ← 3 req/sec token bucket
|
|
273
|
+
└─────────────────────┘
|
|
274
|
+
↓
|
|
275
|
+
Notion API
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Future Extensions
|
|
279
|
+
|
|
280
|
+
- **User Flows**: Auto-populate from controller/route extraction (controller actions, HTTP methods, filters, dependencies)
|
|
281
|
+
- **Data Handling**: Surface column types and model metadata to assist with PII classification
|
|
282
|
+
- **ERD Diagrams**: Generate and attach relationship diagrams to Data Models pages
|
|
283
|
+
- **Diff Reporting**: Use temporal snapshots to show what changed between syncs
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Obsidian Integration
|
|
2
|
+
|
|
3
|
+
Export Woods extraction artifacts to a self-contained [Obsidian](https://obsidian.md) vault, a
|
|
4
|
+
folder of interlinked Markdown notes, one per extracted unit. The vault is designed to be read two
|
|
5
|
+
ways at once:
|
|
6
|
+
|
|
7
|
+
- **By humans**: explore the app's structure in Obsidian's graph view, filter and sort units in an
|
|
8
|
+
Obsidian [Bases](https://help.obsidian.md/bases) table, and drill into a single unit's note with its
|
|
9
|
+
dependencies and dependents as clickable wikilinks.
|
|
10
|
+
- **By agents**: load the entire dependency topology from a single `_woods/` sidecar (one read,
|
|
11
|
+
no per-note fan-out), with a stable `id → note path` manifest for navigation.
|
|
12
|
+
|
|
13
|
+
Unlike the Notion and Unblocked exporters, this one writes **local files only**: there is no API
|
|
14
|
+
token, no network call, and no rate limit. An Obsidian vault is just a folder.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1. Extract (produces tmp/woods/ with the dependency graph + per-unit JSON)
|
|
20
|
+
bundle exec rake woods:extract
|
|
21
|
+
|
|
22
|
+
# 2. Generate the vault (defaults to tmp/woods/obsidian_vault/)
|
|
23
|
+
bundle exec rake woods:obsidian
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then in Obsidian: **Open folder as vault** → point it at `tmp/woods/obsidian_vault`. For the full
|
|
27
|
+
experience (graph colors, Bases, link format) open the generated folder *as its own vault* rather than
|
|
28
|
+
nesting it inside an existing one, a nested folder inherits the host vault's config and ignores the
|
|
29
|
+
shipped `.obsidian/` settings.
|
|
30
|
+
|
|
31
|
+
## What gets generated
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
obsidian_vault/
|
|
35
|
+
├── .woods-vault # ownership sentinel (marks this dir as woods-managed)
|
|
36
|
+
├── .obsidian/ # vault config, only written into a woods-owned vault
|
|
37
|
+
│ ├── app.json # newLinkFormat: absolute, useMarkdownLinks: false
|
|
38
|
+
│ ├── types.json # property types (pagerank → number, tags → tags)
|
|
39
|
+
│ └── graph.json # color groups by #woods/<type> (global graph)
|
|
40
|
+
├── _woods/ # machine sidecar (the agent interface)
|
|
41
|
+
│ ├── manifest.json # { notes: {id → {path,type,pagerank,tags}}, paths: {path → id} }
|
|
42
|
+
│ ├── dependency_graph.json # verbatim copy, full topology for traversal
|
|
43
|
+
│ └── graph_analysis.json # verbatim copy, hubs / cycles / orphans / bridges
|
|
44
|
+
├── Units.base # Obsidian Bases view (filterable/sortable unit inventory)
|
|
45
|
+
├── _Overview.md # top-level map of contents
|
|
46
|
+
├── models/
|
|
47
|
+
│ ├── _index.md # MOC: every model as a wikilink
|
|
48
|
+
│ └── User.md
|
|
49
|
+
├── controllers/
|
|
50
|
+
│ └── Users__RegistrationsController.md
|
|
51
|
+
└── … # one folder per extracted unit type
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## A unit note
|
|
55
|
+
|
|
56
|
+
Each note carries **flat YAML frontmatter** (the machine-queryable surface) and a Markdown body with
|
|
57
|
+
wikilinks (the human surface). Frontmatter is kept flat on purpose. Obsidian's Properties UI does
|
|
58
|
+
not support nested objects or arrays-of-objects, so the structured edge data lives in the `_woods/`
|
|
59
|
+
sidecar instead.
|
|
60
|
+
|
|
61
|
+
```markdown
|
|
62
|
+
---
|
|
63
|
+
woods_managed: true
|
|
64
|
+
id: User
|
|
65
|
+
type: model
|
|
66
|
+
file: app/models/user.rb
|
|
67
|
+
source_hash: a3c5f8e9
|
|
68
|
+
pagerank: 0.0421
|
|
69
|
+
dependency_count: 3
|
|
70
|
+
dependent_count: 12
|
|
71
|
+
tags:
|
|
72
|
+
- woods/model
|
|
73
|
+
- woods/hub
|
|
74
|
+
aliases:
|
|
75
|
+
- User
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
# User
|
|
79
|
+
|
|
80
|
+
**File:** `app/models/user.rb` | **LOC:** 84 | **Table:** users (17 columns)
|
|
81
|
+
|
|
82
|
+
> [!warning] Hub, high blast radius
|
|
83
|
+
> 12 units depend on this (PageRank 0.0421).
|
|
84
|
+
|
|
85
|
+
## Depends on
|
|
86
|
+
- [[models/Account|Account]], *belongs_to*
|
|
87
|
+
- [[mailers/WelcomeMailer|WelcomeMailer]], *job_enqueue*
|
|
88
|
+
|
|
89
|
+
## Used by (12)
|
|
90
|
+
**controllers:** [[controllers/Users__RegistrationsController|Users::RegistrationsController]]
|
|
91
|
+
**jobs:** [[jobs/SyncProfileJob|SyncProfileJob]]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Wikilinks are path-qualified with an alias (`[[models/Account|Account]]`): the target is the
|
|
95
|
+
sanitized vault path so the link always resolves, and the alias shows the original identifier. The
|
|
96
|
+
note's `# H1` carries the clean identifier so the sanitized filename never shows as the title.
|
|
97
|
+
|
|
98
|
+
## The three visualizer surfaces
|
|
99
|
+
|
|
100
|
+
| Surface | Best for | Notes |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| **Graph view** | seeing connections / blast radius at a glance | colored by `#woods/<type>` tag (global graph); for the *local* graph, create the color-by-tag groups once. Obsidian persists them per-vault |
|
|
103
|
+
| **Bases (`Units.base`)** | inventory & triage, "all models sorted by PageRank", "show me the hubs" | requires Obsidian ≥ 1.9 (cards/list views ≥ 1.10); inert and harmless on older versions |
|
|
104
|
+
| **Note bodies + backlinks** | drilling into one unit and navigating outward | "Depends on" / "Used by" are clickable; Obsidian's backlink panel shows inbound links |
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
All options are passed to the rake task via environment variables, there are **no global
|
|
109
|
+
`Woods.configure` settings** for this exporter (the vault path is an output location, not a
|
|
110
|
+
credential):
|
|
111
|
+
|
|
112
|
+
| Env var | Default | Effect |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `WOODS_OUTPUT` | `config.output_dir` (`tmp/woods`) | extraction directory to read from |
|
|
115
|
+
| `WOODS_OBSIDIAN_VAULT` | `<output>/obsidian_vault` | where to write the vault |
|
|
116
|
+
| `WOODS_OBSIDIAN_INCLUDE_SOURCE` | off | embed each unit's source code (credential-scrubbed) in its note |
|
|
117
|
+
| `WOODS_OBSIDIAN_INCLUDE_FRAMEWORK` | off | include `rails_source` units (large; off by default) |
|
|
118
|
+
| `WOODS_OBSIDIAN_FORCE_PURGE` | off | bypass the 30% mass-deletion guard during the stale-note sweep |
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
WOODS_OBSIDIAN_VAULT=~/notes/myapp-code WOODS_OBSIDIAN_INCLUDE_SOURCE=1 bundle exec rake woods:obsidian
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or call the exporter directly:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
require 'woods/obsidian/vault_exporter'
|
|
128
|
+
|
|
129
|
+
Woods::Obsidian::VaultExporter.new(
|
|
130
|
+
index_dir: 'tmp/woods',
|
|
131
|
+
vault_path: 'tmp/woods/obsidian_vault',
|
|
132
|
+
include_source: false,
|
|
133
|
+
include_framework: false
|
|
134
|
+
).export_all
|
|
135
|
+
# => { exported: 412, indexes: 19, swept: 3, skipped: 0, errors: [] }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Re-running and safety
|
|
139
|
+
|
|
140
|
+
The exporter **fully regenerates** the vault on every run (no incremental manifest, local writes are
|
|
141
|
+
cheap). Output is deterministic: re-running against an unchanged extraction produces byte-identical
|
|
142
|
+
notes, so unchanged units never show up in a git diff.
|
|
143
|
+
|
|
144
|
+
Notes Woods manages carry `woods_managed: true` in their frontmatter. On each run, after all notes are
|
|
145
|
+
written successfully, a **sweep** removes managed notes whose unit no longer exists, so deletions in
|
|
146
|
+
your code propagate. Several guards make the sweep safe to point at a real vault:
|
|
147
|
+
|
|
148
|
+
- It only runs against a directory carrying the `.woods-vault` sentinel (written by Woods on first run).
|
|
149
|
+
- It only deletes notes with the `woods_managed: true` marker, your own notes are never touched.
|
|
150
|
+
- It refuses to delete more than 30% of managed notes at once (the signature of a partial extraction)
|
|
151
|
+
unless `WOODS_OBSIDIAN_FORCE_PURGE=1` is set.
|
|
152
|
+
- It resolves symlinks and confirms every deletion target is inside the vault root.
|
|
153
|
+
- It is skipped entirely if any note failed to write that run (a stale note is harmless; a deleted
|
|
154
|
+
reviewed note is not).
|
|
155
|
+
|
|
156
|
+
The same ownership check guards the `.obsidian/` config: Woods will **not** overwrite an existing,
|
|
157
|
+
foreign `.obsidian/` folder, it leaves your Obsidian settings untouched and warns instead.
|
|
158
|
+
|
|
159
|
+
## Limitations
|
|
160
|
+
|
|
161
|
+
- **Bases needs Obsidian ≥ 1.9** (≥ 1.10 for card/list views). The `.base` file is harmless on older
|
|
162
|
+
versions, it simply doesn't render.
|
|
163
|
+
- **`gem_source` units are not exported.** They aren't reachable through the index reader; only
|
|
164
|
+
`rails_source` is covered by `include_framework`.
|
|
165
|
+
- **Hand-edits diverge.** The vault is meant to be regenerated. Editing a note's properties in
|
|
166
|
+
Obsidian rewrites its frontmatter, after which a re-export will overwrite your changes.
|
|
167
|
+
- **Nested vaults ignore the shipped config.** Open the generated folder as its own vault for graph
|
|
168
|
+
colors, Bases, and link-format settings to apply.
|
|
169
|
+
|
|
170
|
+
See `docs/AGENT_GUIDE.md` for how an agent consumes the `_woods/` sidecar.
|