uncoded 1.0.1__tar.gz → 1.2.0__tar.gz

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.
Files changed (82) hide show
  1. uncoded-1.2.0/.uncoded/docs.yaml +34 -0
  2. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/namespace.yaml +160 -25
  3. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/cli.pyi +5 -7
  4. uncoded-1.2.0/.uncoded/stubs/src/uncoded/config.pyi +27 -0
  5. uncoded-1.2.0/.uncoded/stubs/src/uncoded/docs_map.pyi +25 -0
  6. uncoded-1.2.0/.uncoded/stubs/src/uncoded/instruction_files.pyi +25 -0
  7. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/namespace_map.pyi +1 -5
  8. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/skill.pyi +1 -1
  9. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/stubs.pyi +3 -0
  10. uncoded-1.2.0/.uncoded/stubs/src/uncoded/yaml_tree.pyi +10 -0
  11. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_cli.pyi +70 -2
  12. uncoded-1.2.0/.uncoded/stubs/tests/test_config.pyi +71 -0
  13. uncoded-1.2.0/.uncoded/stubs/tests/test_docs_map.pyi +144 -0
  14. uncoded-1.2.0/.uncoded/stubs/tests/test_instruction_files.pyi +109 -0
  15. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_stubs.pyi +26 -1
  16. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_sync.pyi +0 -3
  17. {uncoded-1.0.1 → uncoded-1.2.0}/AGENTS.md +17 -4
  18. {uncoded-1.0.1 → uncoded-1.2.0}/PKG-INFO +42 -9
  19. {uncoded-1.0.1 → uncoded-1.2.0}/README.md +41 -8
  20. {uncoded-1.0.1 → uncoded-1.2.0}/pyproject.toml +1 -0
  21. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/cli.py +152 -84
  22. uncoded-1.2.0/src/uncoded/config.py +133 -0
  23. uncoded-1.2.0/src/uncoded/docs_map.py +154 -0
  24. uncoded-1.2.0/src/uncoded/docs_rule.md +5 -0
  25. uncoded-1.2.0/src/uncoded/instruction_files.py +135 -0
  26. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/namespace_map.py +2 -24
  27. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/skill.py +16 -9
  28. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/stubs.py +37 -0
  29. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/sync.py +2 -4
  30. uncoded-1.2.0/src/uncoded/yaml_tree.py +29 -0
  31. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_cli.py +412 -29
  32. uncoded-1.2.0/tests/test_config.py +204 -0
  33. uncoded-1.2.0/tests/test_docs_map.py +240 -0
  34. uncoded-1.2.0/tests/test_instruction_files.py +380 -0
  35. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_skill.py +15 -15
  36. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_stubs.py +70 -0
  37. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_sync.py +0 -9
  38. uncoded-1.0.1/.uncoded/stubs/src/uncoded/config.pyi +0 -14
  39. uncoded-1.0.1/.uncoded/stubs/src/uncoded/instruction_files.pyi +0 -17
  40. uncoded-1.0.1/.uncoded/stubs/tests/test_config.pyi +0 -36
  41. uncoded-1.0.1/.uncoded/stubs/tests/test_instruction_files.pyi +0 -53
  42. uncoded-1.0.1/src/uncoded/config.py +0 -64
  43. uncoded-1.0.1/src/uncoded/instruction_files.py +0 -64
  44. uncoded-1.0.1/tests/test_config.py +0 -74
  45. uncoded-1.0.1/tests/test_instruction_files.py +0 -122
  46. {uncoded-1.0.1 → uncoded-1.2.0}/.agents/skills/coherence-review/SKILL.md +0 -0
  47. {uncoded-1.0.1 → uncoded-1.2.0}/.claude/skills/coherence-review/SKILL.md +0 -0
  48. {uncoded-1.0.1 → uncoded-1.2.0}/.github/workflows/ci.yml +0 -0
  49. {uncoded-1.0.1 → uncoded-1.2.0}/.github/workflows/publish.yml +0 -0
  50. {uncoded-1.0.1 → uncoded-1.2.0}/.gitignore +0 -0
  51. {uncoded-1.0.1 → uncoded-1.2.0}/.markdownlint-cli2.yaml +0 -0
  52. {uncoded-1.0.1 → uncoded-1.2.0}/.pre-commit-config.yaml +0 -0
  53. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/__init__.pyi +0 -0
  54. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/ast_helpers.pyi +0 -0
  55. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/body.pyi +0 -0
  56. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/extract.pyi +0 -0
  57. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/refs.pyi +0 -0
  58. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/resolver.pyi +0 -0
  59. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/src/uncoded/sync.pyi +0 -0
  60. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_body.pyi +0 -0
  61. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_extract.pyi +0 -0
  62. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_namespace_map.pyi +0 -0
  63. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_refs.pyi +0 -0
  64. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_skill.pyi +0 -0
  65. {uncoded-1.0.1 → uncoded-1.2.0}/.uncoded/stubs/tests/test_uncoded.pyi +0 -0
  66. {uncoded-1.0.1 → uncoded-1.2.0}/CLAUDE.md +0 -0
  67. {uncoded-1.0.1 → uncoded-1.2.0}/LICENSE +0 -0
  68. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/__init__.py +0 -0
  69. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/ast_helpers.py +0 -0
  70. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/body.py +0 -0
  71. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/coherence_review.md +0 -0
  72. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/dispatch_rule.md +0 -0
  73. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/extract.py +0 -0
  74. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/refs.py +0 -0
  75. {uncoded-1.0.1 → uncoded-1.2.0}/src/uncoded/resolver.py +0 -0
  76. {uncoded-1.0.1 → uncoded-1.2.0}/tests/__init__.py +0 -0
  77. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_body.py +0 -0
  78. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_extract.py +0 -0
  79. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_namespace_map.py +0 -0
  80. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_refs.py +0 -0
  81. {uncoded-1.0.1 → uncoded-1.2.0}/tests/test_uncoded.py +0 -0
  82. {uncoded-1.0.1 → uncoded-1.2.0}/uv.lock +0 -0
@@ -0,0 +1,34 @@
1
+ # Documentation index of this codebase, for agent orientation.
2
+ # Generated by uncoded — do not edit; regeneration overwrites.
3
+ #
4
+ # Pure key hierarchy (no lists, no values); indent to zoom in.
5
+ # Directory keys end with "/". Leaf headings map to null.
6
+ #
7
+ # A " (N)" suffix marks the Nth occurrence of a repeated heading at that level.
8
+ # To navigate to it, grep the base heading text and take the Nth match.
9
+
10
+ README.md:
11
+ uncoded:
12
+ What it generates:
13
+ Install uv:
14
+ Configure:
15
+ Use:
16
+ Keep it current with pre-commit:
17
+ Verify the index is fresh:
18
+ Retrieve a symbol body:
19
+ Find references to a symbol:
20
+ How agents use it:
21
+ Coherence review:
22
+ Dev setup:
23
+ Note for Windows contributors:
24
+ Releasing:
25
+ AGENTS.md:
26
+ uncoded:
27
+ Problem:
28
+ Approach:
29
+ Commands:
30
+ How to read and edit code in this codebase:
31
+ The dispatch rule:
32
+ How to execute the rule:
33
+ Where Read, Edit, and grep are still the right tools:
34
+ How to read documentation in this codebase:
@@ -18,15 +18,29 @@ src/:
18
18
  resolve_body:
19
19
  _extract_body:
20
20
  cli.py:
21
- _find_project_root:
22
21
  _sync:
23
22
  _body:
24
23
  _refs:
25
24
  main:
26
25
  config.py:
26
+ ConfigError:
27
+ Config:
28
+ project_root:
29
+ source_roots:
30
+ doc_roots:
31
+ instruction_files:
27
32
  find_pyproject_toml:
28
- read_source_roots:
29
- read_instruction_files:
33
+ _has_uncoded_section:
34
+ _find_config_file:
35
+ read_config:
36
+ docs_map.py:
37
+ DOCS_HEADER:
38
+ extract_headings:
39
+ _unique_key:
40
+ _collapse_empty:
41
+ iter_doc_files:
42
+ build_docs_map:
43
+ render_docs_map:
30
44
  extract.py:
31
45
  ClassInfo:
32
46
  name:
@@ -41,17 +55,21 @@ src/:
41
55
  iter_source_files:
42
56
  extract_modules:
43
57
  instruction_files.py:
44
- MARKER_START:
45
58
  MARKER_END:
59
+ MARKER_DOCS_END:
60
+ MARKER_START_PREFIX:
61
+ MARKER_DOCS_START_PREFIX:
46
62
  DEFAULT_INSTRUCTION_FILES:
47
- _SECTION_BODY:
48
- SECTION:
49
- _replace_or_append:
63
+ _CODE_SECTION_BODY:
64
+ MARKER_START:
65
+ SECTION_CODE:
66
+ _DOCS_SECTION_BODY:
67
+ MARKER_DOCS_START:
68
+ SECTION_DOCS:
69
+ _apply_section:
50
70
  sync_instruction_file:
51
71
  namespace_map.py:
52
72
  HEADER:
53
- _CleanDumper:
54
- increase_indent:
55
73
  build_map:
56
74
  render_map:
57
75
  refs.py:
@@ -131,10 +149,15 @@ src/:
131
149
  render_stub:
132
150
  _generate_stubs:
133
151
  _write_stubs:
152
+ remove_all_stubs:
134
153
  build_stubs:
135
154
  sync.py:
136
155
  sync_file:
137
156
  remove_file:
157
+ yaml_tree.py:
158
+ _CleanDumper:
159
+ increase_indent:
160
+ render_yaml_tree:
138
161
  tests/:
139
162
  test_body.py:
140
163
  TestResolveBodyTopLevel:
@@ -192,10 +215,12 @@ tests/:
192
215
  test_writes_namespace_map_stubs_and_instruction_file:
193
216
  test_idempotent_second_run:
194
217
  test_dedupes_when_claude_md_is_symlink_to_agents_md:
195
- test_instruction_file_outside_project_uses_absolute_path:
196
- test_error_when_no_pyproject_toml:
218
+ test_error_when_instruction_file_outside_project_root:
219
+ test_error_when_no_config_file:
220
+ test_error_when_source_root_outside_project_root:
197
221
  test_error_when_source_root_missing:
198
222
  test_error_when_uncoded_section_missing:
223
+ test_error_when_both_config_files_configure_uncoded:
199
224
  test_skip_warning_emitted_once_per_broken_file:
200
225
  test_anchors_reads_and_writes_at_project_root_when_cwd_is_subdir:
201
226
  test_artefacts_match_when_run_from_subdir_vs_project_root:
@@ -237,20 +262,102 @@ tests/:
237
262
  test_zero_references_exits_zero_with_empty_stdout:
238
263
  test_multiple_references_prints_sorted:
239
264
  test_lsp_failure_exits_one:
265
+ TestSyncDocRoots:
266
+ test_doc_only_writes_docs_yaml:
267
+ test_doc_only_does_not_write_namespace_yaml:
268
+ test_doc_only_does_not_write_stubs:
269
+ test_doc_only_instruction_file_has_docs_section_only:
270
+ test_doc_root_single_md_file:
271
+ test_error_when_doc_root_missing:
272
+ test_error_when_doc_root_is_non_md_file:
273
+ test_error_when_doc_root_outside_project_root:
274
+ test_both_roots_writes_both_artefacts:
275
+ test_both_roots_instruction_file_has_both_sections:
276
+ test_source_root_removal_cleans_code_artefacts:
277
+ test_doc_root_removal_cleans_docs_yaml:
278
+ test_check_returns_one_when_docs_yaml_stale:
279
+ test_check_returns_one_when_stubs_should_be_removed:
280
+ test_check_returns_one_when_docs_yaml_should_be_removed:
281
+ test_doc_only_does_not_write_skill:
282
+ test_doc_only_removes_preexisting_skill:
283
+ test_check_returns_one_when_skill_should_be_removed:
284
+ test_idempotent_doc_only:
240
285
  _init_repo:
286
+ _init_doc_repo:
241
287
  test_config.py:
242
288
  TestFindPyprojectToml:
243
289
  test_finds_at_start:
244
290
  test_finds_in_parent_of_start:
245
291
  test_returns_none_if_not_found:
246
- TestReadSourceRoots:
247
- test_reads_source_roots:
248
- test_raises_if_no_uncoded_section:
249
- TestReadInstructionFiles:
250
- test_returns_default_when_no_pyproject_toml:
251
- test_returns_default_when_key_absent:
252
- test_reads_configured_list:
253
- test_empty_list_is_respected:
292
+ TestReadConfig:
293
+ test_returns_none_when_no_config_file:
294
+ test_source_roots_read_from_pyproject:
295
+ test_doc_roots_read_from_pyproject:
296
+ test_both_roots_empty_when_section_absent:
297
+ test_project_root_is_config_file_parent:
298
+ test_finds_pyproject_in_parent_directory:
299
+ test_instruction_files_default_when_key_absent:
300
+ test_instruction_files_configured:
301
+ test_instruction_files_empty_list_respected:
302
+ test_returns_frozen_dataclass:
303
+ test_finds_uncoded_toml_when_no_pyproject:
304
+ test_uncoded_toml_top_level_keys:
305
+ test_error_when_both_configure_uncoded_in_same_directory:
306
+ test_uncoded_toml_wins_over_bare_pyproject:
307
+ test_uncoded_toml_wins_when_nearer_than_pyproject:
308
+ test_pyproject_wins_when_nearer_than_uncoded_toml:
309
+ test_uncoded_pyproject_and_uncoded_toml_in_different_dirs_no_error:
310
+ test_skips_uncoded_directory_as_config_home:
311
+ test_docs_map.py:
312
+ TestExtractHeadings:
313
+ test_empty_text:
314
+ test_h1_through_h6:
315
+ test_trailing_hashes_stripped:
316
+ test_trailing_hashes_with_surrounding_spaces_stripped:
317
+ test_trailing_hashes_multiple_stripped:
318
+ test_hash_attached_to_word_preserved:
319
+ test_multiple_hashes_attached_to_words_preserved:
320
+ test_hash_attached_to_word_with_closing_sequence:
321
+ test_seven_hashes_not_a_heading:
322
+ test_no_space_after_hash_not_a_heading:
323
+ test_hash_only_not_a_heading:
324
+ test_empty_title_not_a_heading:
325
+ test_setext_underline_equals_not_a_heading:
326
+ test_setext_underline_dashes_not_a_heading:
327
+ test_backtick_fence_suppresses_headings:
328
+ test_tilde_fence_suppresses_headings:
329
+ test_fence_with_info_string:
330
+ test_unclosed_fence_suppresses_rest_of_file:
331
+ test_tilde_does_not_close_backtick_fence:
332
+ test_backtick_does_not_close_tilde_fence:
333
+ test_preserves_order:
334
+ test_title_with_special_chars:
335
+ test_leading_spaces_not_a_heading:
336
+ TestIterDocFiles:
337
+ test_single_file_root:
338
+ test_directory_walk_yields_md_files:
339
+ test_directory_walk_sorted:
340
+ test_directory_excludes_non_md:
341
+ test_directory_nested_subdir:
342
+ test_empty_directory_yields_nothing:
343
+ test_rel_path_is_relative_to_project_root:
344
+ TestBuildDocsMap:
345
+ test_empty_files:
346
+ test_headingless_file_maps_to_null:
347
+ test_single_heading:
348
+ test_nesting_parent_child:
349
+ test_level_skip_attaches_to_nearest_shallower:
350
+ test_heading_returns_to_same_level:
351
+ test_heading_not_starting_at_h1:
352
+ test_duplicate_sibling_disambiguation:
353
+ test_duplicate_increments_past_existing_suffix:
354
+ test_directory_structure_dir_key_has_trailing_slash:
355
+ test_two_files_in_same_directory:
356
+ test_multiple_files_flat:
357
+ TestRenderDocsMap:
358
+ test_header_present:
359
+ test_yaml_parseable:
360
+ test_null_renders_clean:
254
361
  test_extract.py:
255
362
  TestExtractModuleFromSource:
256
363
  test_classes_and_functions:
@@ -280,18 +387,32 @@ tests/:
280
387
  test_module_with_only_constants_is_kept:
281
388
  test_skips_files_with_no_symbols:
282
389
  test_instruction_files.py:
283
- TestSection:
390
+ TestCodeSection:
391
+ test_contains_markers:
392
+ test_markers_in_order:
393
+ test_ends_with_newline:
394
+ TestDocsSection:
284
395
  test_contains_markers:
285
396
  test_markers_in_order:
286
397
  test_ends_with_newline:
398
+ test_mentions_docs_yaml:
287
399
  TestSyncInstructionFile:
288
- test_creates_file_if_missing:
289
- test_appends_to_existing_file:
290
- test_replaces_existing_section:
291
- test_preserves_content_after_section:
400
+ test_creates_file_with_code_section:
401
+ test_appends_code_section_to_existing_file:
402
+ test_replaces_existing_code_section:
403
+ test_preserves_content_after_code_section:
404
+ test_removes_code_section_on_none:
405
+ test_appends_docs_section:
406
+ test_removes_docs_section_on_none:
407
+ test_both_sections_appended_in_order:
408
+ test_both_sections_replaced_independently:
409
+ test_code_only_does_not_write_docs_markers:
410
+ test_docs_only_does_not_write_code_markers:
292
411
  test_idempotent:
293
412
  test_returns_true_on_first_write:
294
413
  test_returns_false_when_clean:
414
+ test_returns_false_when_both_none_and_file_absent:
415
+ test_remove_section_absent_is_noop:
295
416
  TestSyncInstructionFileCheckMode:
296
417
  test_does_not_create_file:
297
418
  test_does_not_update_existing_file:
@@ -299,6 +420,12 @@ tests/:
299
420
  TestSyncInstructionFileProjectRootAnchor:
300
421
  test_project_root_anchors_create_independent_of_cwd:
301
422
  test_project_root_anchors_update_of_existing_file:
423
+ TestSyncInstructionFileFingerprint:
424
+ test_reflowed_body_survives_sync:
425
+ test_reflowed_body_passes_check:
426
+ test_different_fingerprint_refreshes_section:
427
+ test_plain_marker_refreshes_once_then_stable:
428
+ test_prose_mention_of_prefix_before_section_is_ignored:
302
429
  test_namespace_map.py:
303
430
  TestBuildMap:
304
431
  test_single_file:
@@ -432,6 +559,15 @@ tests/:
432
559
  test_project_root_anchors_writes_independent_of_cwd:
433
560
  test_project_root_anchors_orphan_pruning_independent_of_cwd:
434
561
  test_source_root_outside_project_root_skips_cleanup:
562
+ TestRemoveAllStubs:
563
+ _make_stubs:
564
+ test_noop_when_output_dir_absent:
565
+ test_removes_pyi_file_and_directory:
566
+ test_removes_nested_structure:
567
+ test_empty_directory_removed:
568
+ test_check_mode_reports_without_removing:
569
+ test_check_mode_noop_when_absent:
570
+ test_returns_count_of_pyi_files:
435
571
  _setup:
436
572
  _build:
437
573
  test_sync.py:
@@ -452,7 +588,6 @@ tests/:
452
588
  TestSyncFileProjectRootAnchor:
453
589
  test_project_root_anchors_write_independent_of_cwd:
454
590
  test_project_root_preserves_relative_path_in_message:
455
- test_absolute_path_makes_project_root_a_no_op:
456
591
  TestRemoveFileProjectRootAnchor:
457
592
  test_project_root_anchors_removal_independent_of_cwd:
458
593
  test_uncoded.py:
@@ -4,18 +4,16 @@ import argparse
4
4
  import sys
5
5
  from pathlib import Path
6
6
  from uncoded.body import resolve_body
7
- from uncoded.config import find_pyproject_toml, read_instruction_files, read_source_roots
7
+ from uncoded.config import ConfigError, read_config
8
+ from uncoded.docs_map import build_docs_map, iter_doc_files, render_docs_map
8
9
  from uncoded.extract import extract_modules, iter_source_files
9
- from uncoded.instruction_files import sync_instruction_file
10
+ from uncoded.instruction_files import SECTION_CODE, SECTION_DOCS, sync_instruction_file
10
11
  from uncoded.namespace_map import build_map, render_map
11
12
  from uncoded.refs import find_refs
12
13
  from uncoded.resolver import NamePath, SymbolNotFound, UnsupportedNamePath
13
14
  from uncoded.skill import sync_skill
14
- from uncoded.stubs import build_stubs
15
- from uncoded.sync import sync_file
16
-
17
- def _find_project_root(*, start: Path) -> Path | None:
18
- ...
15
+ from uncoded.stubs import build_stubs, remove_all_stubs
16
+ from uncoded.sync import remove_file, sync_file
19
17
 
20
18
  def _sync(*, start: Path | None, check: bool) -> int:
21
19
  ...
@@ -0,0 +1,27 @@
1
+ # src/uncoded/config.py
2
+
3
+ import tomllib
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from uncoded.instruction_files import DEFAULT_INSTRUCTION_FILES
7
+
8
+ def find_pyproject_toml(start: Path) -> Path | None:
9
+ ...
10
+
11
+ def _has_uncoded_section(*, pyproject_path: Path) -> bool:
12
+ ...
13
+
14
+ def _find_config_file(*, start: Path) -> Path | None:
15
+ ...
16
+
17
+ def read_config(start: Path) -> Config | None:
18
+ ...
19
+
20
+ class ConfigError(Exception):
21
+ ...
22
+
23
+ class Config:
24
+ project_root: Path
25
+ source_roots: list[Path]
26
+ doc_roots: list[Path]
27
+ instruction_files: list[Path]
@@ -0,0 +1,25 @@
1
+ # src/uncoded/docs_map.py
2
+
3
+ from collections.abc import Iterable, Iterator
4
+ from pathlib import Path
5
+ from uncoded.yaml_tree import render_yaml_tree
6
+
7
+ DOCS_HEADER = ...
8
+
9
+ def extract_headings(text: str) -> list[tuple[int, str]]:
10
+ ...
11
+
12
+ def _unique_key(*, parent: dict, title: str) -> str:
13
+ ...
14
+
15
+ def _collapse_empty(*, mapping: dict) -> dict | None:
16
+ ...
17
+
18
+ def iter_doc_files(doc_root: Path, project_root: Path) -> Iterator[tuple[str, Path]]:
19
+ ...
20
+
21
+ def build_docs_map(files: Iterable[tuple[str, Path]]) -> dict:
22
+ ...
23
+
24
+ def render_docs_map(mapping: dict) -> str:
25
+ ...
@@ -0,0 +1,25 @@
1
+ # src/uncoded/instruction_files.py
2
+
3
+ import hashlib
4
+ import re
5
+ from importlib.resources import files
6
+ from pathlib import Path
7
+ from uncoded.sync import sync_file
8
+
9
+ MARKER_END = '<!-- uncoded:end -->'
10
+ MARKER_DOCS_END = '<!-- uncoded:docs:end -->'
11
+ MARKER_START_PREFIX = '<!-- uncoded:start'
12
+ MARKER_DOCS_START_PREFIX = '<!-- uncoded:docs:start'
13
+ DEFAULT_INSTRUCTION_FILES = [Path('CLAUDE.md'), Path('AGENTS.md')]
14
+ _CODE_SECTION_BODY = (files('uncoded') / 'dispatch_rule.md').read_text(encoding='utf-8').rstrip('\n')
15
+ MARKER_START = ...
16
+ SECTION_CODE = f'{MARKER_START}\n{_CODE_SECTION_BODY}\n{MARKER_END}\n'
17
+ _DOCS_SECTION_BODY = (files('uncoded') / 'docs_rule.md').read_text(encoding='utf-8').rstrip('\n')
18
+ MARKER_DOCS_START = ...
19
+ SECTION_DOCS = f'{MARKER_DOCS_START}\n{_DOCS_SECTION_BODY}\n{MARKER_DOCS_END}\n'
20
+
21
+ def _apply_section(text: str, start: str, end: str, body: str | None, *, prefix: str) -> str:
22
+ ...
23
+
24
+ def sync_instruction_file(path: Path, *, code_section: str | None, docs_section: str | None, project_root: Path, check: bool) -> bool:
25
+ ...
@@ -1,8 +1,8 @@
1
1
  # src/uncoded/namespace_map.py
2
2
 
3
3
  from pathlib import Path
4
- import yaml
5
4
  from uncoded.extract import ModuleInfo
5
+ from uncoded.yaml_tree import render_yaml_tree
6
6
 
7
7
  HEADER = ...
8
8
 
@@ -11,7 +11,3 @@ def build_map(modules: list[ModuleInfo]) -> dict:
11
11
 
12
12
  def render_map(namespace: dict) -> str:
13
13
  ...
14
-
15
- class _CleanDumper(yaml.SafeDumper):
16
- def increase_indent(self, flow, indentless):
17
- ...
@@ -8,5 +8,5 @@ SKILL_OUTPUTS = ...
8
8
  LEGACY_SKILL_OUTPUTS = ...
9
9
  _SKILL_CONTENT = (files('uncoded') / 'coherence_review.md').read_text(encoding='utf-8')
10
10
 
11
- def sync_skill(*, project_root: Path, check: bool) -> bool:
11
+ def sync_skill(*, project_root: Path, check: bool, build: bool) -> bool:
12
12
  ...
@@ -51,6 +51,9 @@ def _generate_stubs(files: Iterable[tuple[str, str]]) -> dict[Path, str]:
51
51
  def _write_stubs(*, stubs: dict[Path, str], source_root: Path, output_dir: Path, project_root: Path, check: bool) -> int:
52
52
  ...
53
53
 
54
+ def remove_all_stubs(output_dir: Path, *, project_root: Path, check: bool) -> int:
55
+ ...
56
+
54
57
  def build_stubs(*, files: Iterable[tuple[str, str]], source_root: Path, output_dir: Path, project_root: Path, check: bool) -> int:
55
58
  ...
56
59
 
@@ -0,0 +1,10 @@
1
+ # src/uncoded/yaml_tree.py
2
+
3
+ import yaml
4
+
5
+ def render_yaml_tree(header: str, mapping: dict) -> str:
6
+ ...
7
+
8
+ class _CleanDumper(yaml.SafeDumper):
9
+ def increase_indent(self, flow, indentless):
10
+ ...
@@ -6,11 +6,15 @@ from pathlib import Path
6
6
  from unittest import mock
7
7
  import pytest
8
8
  from uncoded import cli
9
+ from uncoded.instruction_files import MARKER_START
9
10
  from uncoded.skill import SKILL_OUTPUTS
10
11
 
11
12
  def _init_repo(tmp_path, monkeypatch, source_roots):
12
13
  ...
13
14
 
15
+ def _init_doc_repo(tmp_path, monkeypatch, doc_roots):
16
+ ...
17
+
14
18
  class TestSyncApplyMode:
15
19
  def test_writes_namespace_map_stubs_and_instruction_file(self, tmp_path, monkeypatch):
16
20
  ...
@@ -21,10 +25,13 @@ class TestSyncApplyMode:
21
25
  def test_dedupes_when_claude_md_is_symlink_to_agents_md(self, tmp_path, monkeypatch, capsys):
22
26
  ...
23
27
 
24
- def test_instruction_file_outside_project_uses_absolute_path(self, tmp_path, monkeypatch):
28
+ def test_error_when_instruction_file_outside_project_root(self, tmp_path, monkeypatch, capsys):
29
+ ...
30
+
31
+ def test_error_when_no_config_file(self, tmp_path, monkeypatch, capsys):
25
32
  ...
26
33
 
27
- def test_error_when_no_pyproject_toml(self, tmp_path, monkeypatch, capsys):
34
+ def test_error_when_source_root_outside_project_root(self, tmp_path, monkeypatch, capsys):
28
35
  ...
29
36
 
30
37
  def test_error_when_source_root_missing(self, tmp_path, monkeypatch, capsys):
@@ -33,6 +40,9 @@ class TestSyncApplyMode:
33
40
  def test_error_when_uncoded_section_missing(self, tmp_path, monkeypatch, capsys):
34
41
  ...
35
42
 
43
+ def test_error_when_both_config_files_configure_uncoded(self, tmp_path, monkeypatch, capsys):
44
+ ...
45
+
36
46
  def test_skip_warning_emitted_once_per_broken_file(self, tmp_path, monkeypatch, capsys):
37
47
  ...
38
48
 
@@ -147,3 +157,61 @@ class TestRefsCommand:
147
157
 
148
158
  def test_lsp_failure_exits_one(self, tmp_path, monkeypatch, capsys):
149
159
  ...
160
+
161
+ class TestSyncDocRoots:
162
+ def test_doc_only_writes_docs_yaml(self, tmp_path, monkeypatch):
163
+ ...
164
+
165
+ def test_doc_only_does_not_write_namespace_yaml(self, tmp_path, monkeypatch):
166
+ ...
167
+
168
+ def test_doc_only_does_not_write_stubs(self, tmp_path, monkeypatch):
169
+ ...
170
+
171
+ def test_doc_only_instruction_file_has_docs_section_only(self, tmp_path, monkeypatch):
172
+ ...
173
+
174
+ def test_doc_root_single_md_file(self, tmp_path, monkeypatch):
175
+ ...
176
+
177
+ def test_error_when_doc_root_missing(self, tmp_path, monkeypatch, capsys):
178
+ ...
179
+
180
+ def test_error_when_doc_root_is_non_md_file(self, tmp_path, monkeypatch, capsys):
181
+ ...
182
+
183
+ def test_error_when_doc_root_outside_project_root(self, tmp_path, monkeypatch, capsys):
184
+ ...
185
+
186
+ def test_both_roots_writes_both_artefacts(self, tmp_path, monkeypatch):
187
+ ...
188
+
189
+ def test_both_roots_instruction_file_has_both_sections(self, tmp_path, monkeypatch):
190
+ ...
191
+
192
+ def test_source_root_removal_cleans_code_artefacts(self, tmp_path, monkeypatch):
193
+ ...
194
+
195
+ def test_doc_root_removal_cleans_docs_yaml(self, tmp_path, monkeypatch):
196
+ ...
197
+
198
+ def test_check_returns_one_when_docs_yaml_stale(self, tmp_path, monkeypatch):
199
+ ...
200
+
201
+ def test_check_returns_one_when_stubs_should_be_removed(self, tmp_path, monkeypatch):
202
+ ...
203
+
204
+ def test_check_returns_one_when_docs_yaml_should_be_removed(self, tmp_path, monkeypatch):
205
+ ...
206
+
207
+ def test_doc_only_does_not_write_skill(self, tmp_path, monkeypatch):
208
+ ...
209
+
210
+ def test_doc_only_removes_preexisting_skill(self, tmp_path, monkeypatch):
211
+ ...
212
+
213
+ def test_check_returns_one_when_skill_should_be_removed(self, tmp_path, monkeypatch):
214
+ ...
215
+
216
+ def test_idempotent_doc_only(self, tmp_path, monkeypatch):
217
+ ...
@@ -0,0 +1,71 @@
1
+ # tests/test_config.py
2
+
3
+ from pathlib import Path
4
+ import pytest
5
+ from uncoded.config import Config, ConfigError, find_pyproject_toml, read_config
6
+ from uncoded.instruction_files import DEFAULT_INSTRUCTION_FILES
7
+
8
+ class TestFindPyprojectToml:
9
+ def test_finds_at_start(self, tmp_path):
10
+ ...
11
+
12
+ def test_finds_in_parent_of_start(self, tmp_path):
13
+ ...
14
+
15
+ def test_returns_none_if_not_found(self, tmp_path):
16
+ ...
17
+
18
+ class TestReadConfig:
19
+ def test_returns_none_when_no_config_file(self, tmp_path):
20
+ ...
21
+
22
+ def test_source_roots_read_from_pyproject(self, tmp_path):
23
+ ...
24
+
25
+ def test_doc_roots_read_from_pyproject(self, tmp_path):
26
+ ...
27
+
28
+ def test_both_roots_empty_when_section_absent(self, tmp_path):
29
+ ...
30
+
31
+ def test_project_root_is_config_file_parent(self, tmp_path):
32
+ ...
33
+
34
+ def test_finds_pyproject_in_parent_directory(self, tmp_path):
35
+ ...
36
+
37
+ def test_instruction_files_default_when_key_absent(self, tmp_path):
38
+ ...
39
+
40
+ def test_instruction_files_configured(self, tmp_path):
41
+ ...
42
+
43
+ def test_instruction_files_empty_list_respected(self, tmp_path):
44
+ ...
45
+
46
+ def test_returns_frozen_dataclass(self, tmp_path):
47
+ ...
48
+
49
+ def test_finds_uncoded_toml_when_no_pyproject(self, tmp_path):
50
+ ...
51
+
52
+ def test_uncoded_toml_top_level_keys(self, tmp_path):
53
+ ...
54
+
55
+ def test_error_when_both_configure_uncoded_in_same_directory(self, tmp_path):
56
+ ...
57
+
58
+ def test_uncoded_toml_wins_over_bare_pyproject(self, tmp_path):
59
+ ...
60
+
61
+ def test_uncoded_toml_wins_when_nearer_than_pyproject(self, tmp_path):
62
+ ...
63
+
64
+ def test_pyproject_wins_when_nearer_than_uncoded_toml(self, tmp_path):
65
+ ...
66
+
67
+ def test_uncoded_pyproject_and_uncoded_toml_in_different_dirs_no_error(self, tmp_path):
68
+ ...
69
+
70
+ def test_skips_uncoded_directory_as_config_home(self, tmp_path):
71
+ ...