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,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
|
|
8
|
+
require_relative 'atomic_file'
|
|
9
|
+
|
|
10
|
+
module Woods
|
|
11
|
+
# A monotonic marker for "which version of the index is on disk".
|
|
12
|
+
#
|
|
13
|
+
# The index is a directory of files, so a reader has no cheap way to ask
|
|
14
|
+
# "has this changed since I cached it?" — it would have to stat everything.
|
|
15
|
+
# This is that cheap way: one small file, bumped as the *last* step of any
|
|
16
|
+
# write, holding a counter that only ever increases plus a token that
|
|
17
|
+
# changes whenever the counter does.
|
|
18
|
+
#
|
|
19
|
+
# Two properties matter, and both come from #164's central rule — never
|
|
20
|
+
# advance a cursor over work that didn't land:
|
|
21
|
+
#
|
|
22
|
+
# * **Bumped last.** Callers write the index, then bump. A reader that sees
|
|
23
|
+
# generation N therefore knows the files for N are already there.
|
|
24
|
+
# * **Not bumped on failure.** A run that raised, or that changed nothing,
|
|
25
|
+
# leaves the generation alone. Staleness is then honest: the reader keeps
|
|
26
|
+
# serving N and knows it.
|
|
27
|
+
#
|
|
28
|
+
# The write itself is atomic ({AtomicFile}), so a reader never sees a torn
|
|
29
|
+
# generation file. It can also carry a **payload pointer** — the name of an
|
|
30
|
+
# immutable per-generation directory the payload was published into. When
|
|
31
|
+
# present, the single atomic write of this file is the one commit point for
|
|
32
|
+
# the whole payload: a reader resolves every artifact of a read through the
|
|
33
|
+
# directory this generation names, so it sees one generation whole rather
|
|
34
|
+
# than a mix of a manifest from N+1 next to a unit from N. The pointer is
|
|
35
|
+
# optional — an index written flat (payload files directly under the output
|
|
36
|
+
# directory) carries none, and readers fall back to the output directory.
|
|
37
|
+
#
|
|
38
|
+
# @example Publishing after a successful write
|
|
39
|
+
# Woods::Generation.new(output_dir: "tmp/woods").bump!(reason: "incremental")
|
|
40
|
+
# # => #<Generation::Marker number=42 token="9f2c…" reason="incremental">
|
|
41
|
+
#
|
|
42
|
+
class Generation
|
|
43
|
+
FILENAME = 'generation.json'
|
|
44
|
+
|
|
45
|
+
# An immutable snapshot of the generation file.
|
|
46
|
+
#
|
|
47
|
+
# @!attribute number
|
|
48
|
+
# @return [Integer] monotonically increasing, starting at 1
|
|
49
|
+
# @!attribute token
|
|
50
|
+
# @return [String] opaque; changes on every bump
|
|
51
|
+
# @!attribute updated_at
|
|
52
|
+
# @return [String, nil] ISO8601 stamp of the bump
|
|
53
|
+
# @!attribute reason
|
|
54
|
+
# @return [String, nil] what produced this generation
|
|
55
|
+
# @!attribute payload
|
|
56
|
+
# @return [String, nil] name of the immutable directory this generation's
|
|
57
|
+
# payload lives in, relative to the output directory. nil for an index
|
|
58
|
+
# written flat (payload files directly under the output directory).
|
|
59
|
+
Marker = Struct.new(:number, :token, :updated_at, :reason, :payload, keyword_init: true) do
|
|
60
|
+
# @return [Hash] string-keyed, ready to serialize. The +payload+ key is
|
|
61
|
+
# emitted only when set, so a flat index's generation file is byte-for-
|
|
62
|
+
# byte what it always was.
|
|
63
|
+
def to_h
|
|
64
|
+
base = { 'number' => number, 'token' => token, 'updated_at' => updated_at, 'reason' => reason }
|
|
65
|
+
base['payload'] = payload if payload
|
|
66
|
+
base
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Marker returned when no generation file exists yet — an index written
|
|
71
|
+
# before generations existed, or a directory nothing has published to.
|
|
72
|
+
UNPUBLISHED = Marker.new(number: 0, token: nil, updated_at: nil, reason: nil).freeze
|
|
73
|
+
|
|
74
|
+
# @param output_dir [String, Pathname] index directory
|
|
75
|
+
# @param clock [#call] returns the ISO8601 stamp for a bump; injectable
|
|
76
|
+
# so specs don't depend on wall-clock time
|
|
77
|
+
def initialize(output_dir:, clock: -> { Time.now.utc.iso8601 })
|
|
78
|
+
@path = File.join(output_dir.to_s, FILENAME)
|
|
79
|
+
@clock = clock
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [String] absolute path to the generation file
|
|
83
|
+
attr_reader :path
|
|
84
|
+
|
|
85
|
+
# The generation currently published.
|
|
86
|
+
#
|
|
87
|
+
# A missing or unreadable file reads as {UNPUBLISHED} rather than raising:
|
|
88
|
+
# a reader asking "what generation is this?" of a directory that has none
|
|
89
|
+
# wants an answer, not an exception.
|
|
90
|
+
#
|
|
91
|
+
# @return [Marker]
|
|
92
|
+
def current
|
|
93
|
+
return UNPUBLISHED unless File.exist?(@path)
|
|
94
|
+
|
|
95
|
+
data = JSON.parse(AtomicFile.read(@path))
|
|
96
|
+
Marker.new(
|
|
97
|
+
number: data['number'].to_i,
|
|
98
|
+
token: data['token'],
|
|
99
|
+
updated_at: data['updated_at'],
|
|
100
|
+
reason: data['reason'],
|
|
101
|
+
payload: data['payload']
|
|
102
|
+
)
|
|
103
|
+
rescue JSON::ParserError, SystemCallError
|
|
104
|
+
UNPUBLISHED
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Advance to the next generation.
|
|
108
|
+
#
|
|
109
|
+
# Call this *after* the index files are written, never before, and never
|
|
110
|
+
# after a run that failed or wrote nothing.
|
|
111
|
+
#
|
|
112
|
+
# @param reason [String, nil] what produced this generation, for humans
|
|
113
|
+
# reading `woods_status`
|
|
114
|
+
# @param payload [String, nil] name of the immutable directory this
|
|
115
|
+
# generation's payload was published into, relative to the output
|
|
116
|
+
# directory. Omit for a flat index; when given, this bump becomes the
|
|
117
|
+
# single commit point for that whole payload (see the class docs).
|
|
118
|
+
# @return [Marker] the newly published generation
|
|
119
|
+
def bump!(reason: nil, payload: nil)
|
|
120
|
+
marker = Marker.new(
|
|
121
|
+
number: current.number + 1,
|
|
122
|
+
token: SecureRandom.hex(8),
|
|
123
|
+
updated_at: @clock.call,
|
|
124
|
+
reason: reason,
|
|
125
|
+
payload: payload
|
|
126
|
+
)
|
|
127
|
+
AtomicFile.write(@path, JSON.generate(marker.to_h))
|
|
128
|
+
marker
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The directory a generation's payload artifacts live in.
|
|
132
|
+
#
|
|
133
|
+
# Returns the index root when there is no pointer (an index written flat,
|
|
134
|
+
# which is every index written before payloads existed), when the named
|
|
135
|
+
# directory is gone (a stale pointer — a payload retention pruned), or
|
|
136
|
+
# when the name would escape the index root. A torn or tampered pointer
|
|
137
|
+
# must degrade to the flat layout, never read outside the index; the name
|
|
138
|
+
# is written by Woods and is relative by contract.
|
|
139
|
+
#
|
|
140
|
+
# Every reader of a payload artifact resolves through this, so the pointer
|
|
141
|
+
# is followed identically wherever it is read.
|
|
142
|
+
#
|
|
143
|
+
# @param marker [Marker] the generation to resolve; defaults to the
|
|
144
|
+
# published one. Pass an already-loaded marker to avoid re-reading, and
|
|
145
|
+
# to resolve a generation a caller has pinned rather than the current one.
|
|
146
|
+
# @return [Pathname]
|
|
147
|
+
def payload_dir(marker = current)
|
|
148
|
+
name = marker.payload
|
|
149
|
+
return root if name.nil? || name.empty?
|
|
150
|
+
|
|
151
|
+
candidate = root.join(name)
|
|
152
|
+
return root unless candidate.directory?
|
|
153
|
+
return root unless within_root?(candidate)
|
|
154
|
+
|
|
155
|
+
candidate
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @return [Pathname] the index directory this generation lives in
|
|
159
|
+
def root
|
|
160
|
+
@root ||= Pathname.new(File.dirname(@path))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# Does +candidate+ resolve inside the index root?
|
|
166
|
+
#
|
|
167
|
+
# Compared through +realpath+, mirroring
|
|
168
|
+
# {Woods::IndexArtifact#validate_dump_dir!} (B-134). +expand_path+ is
|
|
169
|
+
# textual, so a symlink planted *inside* +payloads/+ and pointing outside
|
|
170
|
+
# the index passed the check while +Pathname#directory?+ happily followed
|
|
171
|
+
# it — aiming every payload reader at an arbitrary directory (CORE-6). An
|
|
172
|
+
# index reached through a symlinked root still resolves, because both
|
|
173
|
+
# sides go through realpath.
|
|
174
|
+
#
|
|
175
|
+
# @param candidate [Pathname] an existing directory
|
|
176
|
+
# @return [Boolean] false when it cannot be resolved at all
|
|
177
|
+
def within_root?(candidate)
|
|
178
|
+
candidate_real = candidate.realpath.to_s
|
|
179
|
+
root_real = root.realpath.to_s
|
|
180
|
+
candidate_real.start_with?("#{root_real}#{File::SEPARATOR}")
|
|
181
|
+
rescue SystemCallError
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Woods
|
|
4
|
+
# The one place a Woods git command line is built.
|
|
5
|
+
#
|
|
6
|
+
# Every git call Woods makes is rooted at the extracted application rather
|
|
7
|
+
# than at the process working directory, so extraction launched from another
|
|
8
|
+
# checkout never reports that checkout's history.
|
|
9
|
+
#
|
|
10
|
+
# `WOODS_GIT_DIR` wins when set. It is the escape hatch for a container over
|
|
11
|
+
# a linked worktree: `.git` there is a file naming an absolute host path that
|
|
12
|
+
# may not be mounted, and git's own `GIT_DIR` is not enough, because a
|
|
13
|
+
# worktree's private git directory reaches the shared object store through a
|
|
14
|
+
# relative `commondir` pointer that resolves outside the mount, which
|
|
15
|
+
# `GIT_COMMON_DIR` does not override (B-181, B-186).
|
|
16
|
+
#
|
|
17
|
+
# Three call sites use this, and the documentation promises all three:
|
|
18
|
+
# per-unit enrichment (`Extractor`), manifest provenance (`GitProvenance`),
|
|
19
|
+
# and the `woods:incremental` diff range (`lib/tasks/woods.rake`).
|
|
20
|
+
module GitCommand
|
|
21
|
+
# Environment variable naming the canonical git directory.
|
|
22
|
+
OVERRIDE_KEY = 'WOODS_GIT_DIR'
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# @param root [String, Pathname] repository root the command runs against
|
|
27
|
+
# @param args [Array<String>] git arguments, global options included
|
|
28
|
+
# @param env [Hash] environment source (overridable in specs)
|
|
29
|
+
# @return [Array<String>] full argv for `Open3`
|
|
30
|
+
def argv(root, *args, env: ENV)
|
|
31
|
+
root = root.to_s
|
|
32
|
+
override = env[OVERRIDE_KEY]
|
|
33
|
+
return ['git', '-C', root, *args] if override.nil? || override.empty?
|
|
34
|
+
|
|
35
|
+
['git', '--git-dir', override, '--work-tree', root, '-C', root, *args]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/woods/git_provenance.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
|
+
require_relative 'git_command'
|
|
4
5
|
|
|
5
6
|
module Woods
|
|
6
7
|
# Resolves git provenance (branch + commit SHA) for the extracted codebase,
|
|
@@ -78,12 +79,25 @@ module Woods
|
|
|
78
79
|
#
|
|
79
80
|
# @return [String] stripped output, or empty string on any failure
|
|
80
81
|
def rev_parse(*args)
|
|
81
|
-
out, status = Open3.
|
|
82
|
+
out, _err, status = Open3.capture3(*git_argv('rev-parse', *args))
|
|
82
83
|
status.success? ? out.strip : ''
|
|
83
84
|
rescue StandardError
|
|
84
85
|
''
|
|
85
86
|
end
|
|
86
87
|
|
|
88
|
+
# +WOODS_GIT_DIR+ names the canonical git directory outright and wins when
|
|
89
|
+
# set. It is the escape hatch for a container that can mount that
|
|
90
|
+
# directory but not the host path a linked worktree's +.git+ file points
|
|
91
|
+
# at; git's own +GIT_DIR+ is not enough there, because a worktree's
|
|
92
|
+
# private git directory reaches the shared one through a relative
|
|
93
|
+
# +commondir+ pointer that resolves outside the mount.
|
|
94
|
+
#
|
|
95
|
+
# @param args [Array<String>] git arguments
|
|
96
|
+
# @return [Array<String>] full argv
|
|
97
|
+
def git_argv(*args)
|
|
98
|
+
GitCommand.argv(@root, *args, env: @env)
|
|
99
|
+
end
|
|
100
|
+
|
|
87
101
|
# Decide what to emit when git resolution produced nothing. A baked +GIT_*+
|
|
88
102
|
# env var is honored only when this checkout is not a git working tree (no
|
|
89
103
|
# +.git+ at the root) or git is unavailable. When git is present AND a +.git+
|
|
@@ -103,7 +117,7 @@ module Woods
|
|
|
103
117
|
return @git_available if defined?(@git_available)
|
|
104
118
|
|
|
105
119
|
@git_available = begin
|
|
106
|
-
_out, status = Open3.
|
|
120
|
+
_out, _err, status = Open3.capture3('git', '--version')
|
|
107
121
|
status.success?
|
|
108
122
|
rescue StandardError
|
|
109
123
|
false
|