Skip to content

Commit 42ff1de

Browse files
fix: slug formatting and mermaid support
1 parent 18bdbd9 commit 42ff1de

File tree

20 files changed

+865
-360
lines changed

20 files changed

+865
-360
lines changed

.github/workflows/generate-docs.yml

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
generate:
1111
runs-on: ubuntu-latest
1212
permissions:
13-
contents: write
13+
contents: read
1414

1515
steps:
1616
- name: Checkout clore
@@ -24,14 +24,16 @@ jobs:
2424
cache: false
2525
locked: true
2626

27-
- name: Cache .clice
28-
uses: actions/cache@v5
27+
- name: Restore .clice cache
28+
id: restore-clice-cache
29+
uses: actions/cache/restore@v5
2930
with:
3031
path: .clice/
3132
key: ${{ runner.os }}-clice
3233

33-
- name: Cache build directory
34-
uses: actions/cache@v5
34+
- name: Restore build directory cache
35+
id: restore-build-cache
36+
uses: actions/cache/restore@v5
3537
with:
3638
path: build/Debug/
3739
key: ${{ runner.os }}-${{ hashFiles('pixi.lock', 'pixi.toml', '**/CMakeLists.txt', 'cmake/**/*.cmake') }}
@@ -42,7 +44,22 @@ jobs:
4244
- name: Build
4345
run: pixi run cmake-build Debug
4446

47+
- name: Save .clice cache
48+
if: steps.restore-clice-cache.outputs.cache-hit != 'true'
49+
uses: actions/cache/save@v5
50+
with:
51+
path: .clice/
52+
key: ${{ steps.restore-clice-cache.outputs.cache-primary-key }}
53+
54+
- name: Save build directory cache
55+
if: steps.restore-build-cache.outputs.cache-hit != 'true'
56+
uses: actions/cache/save@v5
57+
with:
58+
path: build/Debug/
59+
key: ${{ steps.restore-build-cache.outputs.cache-primary-key }}
60+
4561
- name: Generate documentation
62+
if: github.event_name == 'push'
4663
env:
4764
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
4865
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

src/config/validate.cppm

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,6 @@ auto validate(const TaskConfig& config) -> std::expected<void, ValidationError>
167167
if(auto r = validate_nonzero(config.evidence_rules.max_related_summaries, "evidence_rules.max_related_summaries"); !r) return r;
168168

169169
// Validate workflow rules
170-
if(auto r = validate_nonzero(config.workflow_rules.min_chain_symbols,
171-
"workflow_rules.min_chain_symbols");
172-
!r) return r;
173170
if(config.workflow_rules.min_chain_symbols < 2) {
174171
return std::unexpected(ValidationError{
175172
.message =

src/extract/cache.cppm

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ struct CacheRecord {
4848

4949
auto build_compile_signature(const CompileEntry& entry) -> std::uint64_t;
5050

51-
auto hash_file(std::string_view path) -> std::uint64_t;
51+
auto hash_file(std::string_view path) -> std::expected<std::uint64_t, CacheError>;
5252

5353
auto capture_dependency_snapshot(const std::vector<std::string>& files)
54-
-> DependencySnapshot;
54+
-> std::expected<DependencySnapshot, CacheError>;
5555

5656
auto dependencies_changed(const DependencySnapshot& snapshot) -> bool;
5757

@@ -187,16 +187,19 @@ auto build_compile_signature(const CompileEntry& entry) -> std::uint64_t {
187187
return llvm::xxh3_64bits(payload);
188188
}
189189

190-
auto hash_file(std::string_view path) -> std::uint64_t {
190+
auto hash_file(std::string_view path) -> std::expected<std::uint64_t, CacheError> {
191191
auto buffer = llvm::MemoryBuffer::getFile(path);
192192
if(!buffer) {
193-
return 0;
193+
return std::unexpected(CacheError{
194+
.message = std::format("failed to read '{}' for hashing: {}",
195+
path, buffer.getError().message()),
196+
});
194197
}
195198
return llvm::xxh3_64bits((*buffer)->getBuffer());
196199
}
197200

198201
auto capture_dependency_snapshot(const std::vector<std::string>& files)
199-
-> DependencySnapshot {
202+
-> std::expected<DependencySnapshot, CacheError> {
200203
DependencySnapshot snapshot;
201204
snapshot.build_at = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
202205

@@ -208,14 +211,25 @@ auto capture_dependency_snapshot(const std::vector<std::string>& files)
208211
snapshot.hashes.reserve(normalized.size());
209212
for(const auto& file : normalized) {
210213
auto normalized_path = normalize_path_string(file);
214+
auto hash = hash_file(normalized_path);
215+
if(!hash.has_value()) {
216+
return std::unexpected(CacheError{
217+
.message = std::format("failed to hash dependency '{}': {}",
218+
normalized_path, hash.error().message),
219+
});
220+
}
211221
snapshot.files.push_back(normalized_path);
212-
snapshot.hashes.push_back(hash_file(normalized_path));
222+
snapshot.hashes.push_back(*hash);
213223
}
214224

215225
return snapshot;
216226
}
217227

218228
auto dependencies_changed(const DependencySnapshot& snapshot) -> bool {
229+
if(snapshot.build_at <= 0 || snapshot.files.empty()) {
230+
return true;
231+
}
232+
219233
if(snapshot.files.size() != snapshot.hashes.size()) {
220234
return true;
221235
}
@@ -235,7 +249,8 @@ auto dependencies_changed(const DependencySnapshot& snapshot) -> bool {
235249
continue;
236250
}
237251

238-
if(hash_file(file) != snapshot.hashes[index]) {
252+
auto hash = hash_file(file);
253+
if(!hash.has_value() || *hash != snapshot.hashes[index]) {
239254
return true;
240255
}
241256
}

src/extract/extract.cppm

Lines changed: 29 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export import :scan;
2121
export import :tooling;
2222
export import :ast;
2323
export import :cache;
24+
export import :qualified_name;
2425

2526
import config;
2627
import support;
@@ -136,7 +137,7 @@ using Ms = std::chrono::milliseconds;
136137

137138
struct CacheEvaluation {
138139
std::uint64_t compile_signature = 0;
139-
std::uint64_t source_hash = 0;
140+
std::optional<std::uint64_t> source_hash;
140141
bool scan_valid = false;
141142
bool ast_valid = false;
142143
};
@@ -161,65 +162,6 @@ void deduplicate(std::vector<T>& values) {
161162
values.erase(std::unique(values.begin(), values.end()), values.end());
162163
}
163164

164-
auto split_top_level_qualified_name(std::string_view qualified_name)
165-
-> std::vector<std::string> {
166-
std::vector<std::string> parts;
167-
std::string current;
168-
current.reserve(qualified_name.size());
169-
170-
std::size_t template_depth = 0;
171-
for(std::size_t index = 0; index < qualified_name.size(); ++index) {
172-
auto ch = qualified_name[index];
173-
if(ch == '<') {
174-
template_depth++;
175-
current.push_back(ch);
176-
continue;
177-
}
178-
if(ch == '>') {
179-
if(template_depth > 0) {
180-
template_depth--;
181-
}
182-
current.push_back(ch);
183-
continue;
184-
}
185-
if(ch == ':' && template_depth == 0 && index + 1 < qualified_name.size() &&
186-
qualified_name[index + 1] == ':') {
187-
parts.push_back(current);
188-
current.clear();
189-
++index;
190-
continue;
191-
}
192-
current.push_back(ch);
193-
}
194-
195-
if(!current.empty()) {
196-
parts.push_back(std::move(current));
197-
}
198-
199-
return parts;
200-
}
201-
202-
auto join_qualified_name_parts(const std::vector<std::string>& parts,
203-
std::size_t count) -> std::string {
204-
std::string joined;
205-
for(std::size_t index = 0; index < count; ++index) {
206-
if(index > 0) {
207-
joined += "::";
208-
}
209-
joined += parts[index];
210-
}
211-
return joined;
212-
}
213-
214-
auto namespace_prefix_from_qualified_name(std::string_view qualified_name)
215-
-> std::string {
216-
auto parts = split_top_level_qualified_name(qualified_name);
217-
if(parts.size() <= 1) {
218-
return {};
219-
}
220-
return join_qualified_name_parts(parts, parts.size() - 1);
221-
}
222-
223165
auto ensure_namespace_hierarchy(ProjectModel& model, std::string_view namespace_name)
224166
-> std::string {
225167
if(namespace_name.empty() ||
@@ -506,21 +448,23 @@ auto extract_project(const config::TaskConfig& config)
506448
auto normalized = std::filesystem::path(entry.file).lexically_normal().generic_string();
507449
auto compile_signature = cache::build_compile_signature(entry);
508450
auto source_hash = cache::hash_file(normalized);
509-
if(source_hash == 0) {
510-
return std::unexpected(ExtractError{
511-
.message = std::format("failed to hash source file for extract cache: {}",
512-
normalized)});
451+
if(!source_hash.has_value()) {
452+
logging::warn("extract cache disabled for {}: {}",
453+
normalized, source_hash.error().message);
513454
}
514455

515456
CacheEvaluation evaluation{
516457
.compile_signature = compile_signature,
517-
.source_hash = source_hash,
518458
};
459+
if(source_hash.has_value()) {
460+
evaluation.source_hash = *source_hash;
461+
}
519462

520463
if(auto cache_it = cache_records.find(normalized); cache_it != cache_records.end()) {
521464
const auto& record = cache_it->second;
522-
if(record.compile_signature == compile_signature &&
523-
record.source_hash == source_hash) {
465+
if(evaluation.source_hash.has_value() &&
466+
record.compile_signature == compile_signature &&
467+
record.source_hash == *evaluation.source_hash) {
524468
seeded_scan_cache.insert_or_assign(normalized, record.scan);
525469
evaluation.scan_valid = true;
526470

@@ -596,7 +540,13 @@ auto extract_project(const config::TaskConfig& config)
596540
entry.file, ast_result.error().message)});
597541
}
598542
ast_data = std::move(*ast_result);
599-
ast_deps_snapshot = cache::capture_dependency_snapshot(ast_data.dependencies);
543+
auto ast_deps_result = cache::capture_dependency_snapshot(ast_data.dependencies);
544+
if(!ast_deps_result.has_value()) {
545+
logging::warn("failed to capture AST dependency snapshot for {}: {}",
546+
normalized, ast_deps_result.error().message);
547+
} else {
548+
ast_deps_snapshot = std::move(*ast_deps_result);
549+
}
600550

601551
auto dt_ast = std::chrono::duration_cast<Ms>(Clock::now() - t_ast);
602552
logging::info(" ast: {} symbols, {} relations ({}ms)",
@@ -685,13 +635,17 @@ auto extract_project(const config::TaskConfig& config)
685635
}
686636
}
687637

688-
cache_records.insert_or_assign(normalized, cache::CacheRecord{
689-
.compile_signature = cache_state_it->second.compile_signature,
690-
.source_hash = cache_state_it->second.source_hash,
691-
.ast_deps = std::move(ast_deps_snapshot),
692-
.scan = cache_it->second,
693-
.ast = std::move(cached_ast_copy),
694-
});
638+
if(cache_state_it->second.source_hash.has_value()) {
639+
cache_records.insert_or_assign(normalized, cache::CacheRecord{
640+
.compile_signature = cache_state_it->second.compile_signature,
641+
.source_hash = *cache_state_it->second.source_hash,
642+
.ast_deps = std::move(ast_deps_snapshot),
643+
.scan = cache_it->second,
644+
.ast = std::move(cached_ast_copy),
645+
});
646+
} else {
647+
cache_records.erase(normalized);
648+
}
695649

696650
auto dt_file = std::chrono::duration_cast<Ms>(Clock::now() - t_file);
697651
logging::info(" kept: {} symbols, {} includes ({}ms)",

src/extract/qualified_name.cppm

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
module;
2+
3+
#include <cstddef>
4+
#include <string>
5+
#include <string_view>
6+
#include <vector>
7+
8+
export module extract:qualified_name;
9+
10+
export namespace clore::extract {
11+
12+
auto split_top_level_qualified_name(std::string_view qualified_name)
13+
-> std::vector<std::string>;
14+
15+
auto join_qualified_name_parts(const std::vector<std::string>& parts,
16+
std::size_t count) -> std::string;
17+
18+
auto namespace_prefix_from_qualified_name(std::string_view qualified_name)
19+
-> std::string;
20+
21+
} // namespace clore::extract
22+
23+
namespace clore::extract {
24+
25+
auto split_top_level_qualified_name(std::string_view qualified_name)
26+
-> std::vector<std::string> {
27+
std::vector<std::string> parts;
28+
std::string current;
29+
current.reserve(qualified_name.size());
30+
31+
std::size_t template_depth = 0;
32+
for(std::size_t index = 0; index < qualified_name.size(); ++index) {
33+
auto ch = qualified_name[index];
34+
if(ch == '<') {
35+
template_depth++;
36+
current.push_back(ch);
37+
continue;
38+
}
39+
if(ch == '>') {
40+
if(template_depth > 0) {
41+
template_depth--;
42+
}
43+
current.push_back(ch);
44+
continue;
45+
}
46+
if(ch == ':' && template_depth == 0 && index + 1 < qualified_name.size() &&
47+
qualified_name[index + 1] == ':') {
48+
parts.push_back(current);
49+
current.clear();
50+
++index;
51+
continue;
52+
}
53+
current.push_back(ch);
54+
}
55+
56+
if(!current.empty()) {
57+
parts.push_back(std::move(current));
58+
}
59+
60+
return parts;
61+
}
62+
63+
auto join_qualified_name_parts(const std::vector<std::string>& parts,
64+
std::size_t count) -> std::string {
65+
std::string joined;
66+
for(std::size_t index = 0; index < count; ++index) {
67+
if(index > 0) {
68+
joined += "::";
69+
}
70+
joined += parts[index];
71+
}
72+
return joined;
73+
}
74+
75+
auto namespace_prefix_from_qualified_name(std::string_view qualified_name)
76+
-> std::string {
77+
auto parts = split_top_level_qualified_name(qualified_name);
78+
if(parts.size() <= 1) {
79+
return {};
80+
}
81+
return join_qualified_name_parts(parts, parts.size() - 1);
82+
}
83+
84+
} // namespace clore::extract

0 commit comments

Comments
 (0)