salak 0.1.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.
- salak-0.1.0/.cursorrules +69 -0
- salak-0.1.0/.gitattributes +16 -0
- salak-0.1.0/.github/copilot-instructions.md +84 -0
- salak-0.1.0/.github/workflows/ci.yml +96 -0
- salak-0.1.0/.github/workflows/clean-machine.yml +138 -0
- salak-0.1.0/.github/workflows/publish-testpypi.yml +71 -0
- salak-0.1.0/.github/workflows/release.yml +96 -0
- salak-0.1.0/.gitignore +36 -0
- salak-0.1.0/AGENTS.md +84 -0
- salak-0.1.0/CLAUDE.md +561 -0
- salak-0.1.0/LICENSE +21 -0
- salak-0.1.0/PKG-INFO +234 -0
- salak-0.1.0/README.md +214 -0
- salak-0.1.0/context-pack.md +279 -0
- salak-0.1.0/docs/RELEASING.md +152 -0
- salak-0.1.0/docs/SALAK-AUDIT-2026-07-29.md +84 -0
- salak-0.1.0/docs/SALAK-SDD-v1.0.md +411 -0
- salak-0.1.0/docs/SALAK-SDD-v1_1.md +620 -0
- salak-0.1.0/docs/SALAK-SDD-v1_2.md +660 -0
- salak-0.1.0/docs/SALAK-SDD-v1_3.md +850 -0
- salak-0.1.0/docs/SALAK-SDD-v1_4.md +1431 -0
- salak-0.1.0/docs/cli-reference.md +382 -0
- salak-0.1.0/docs/salak-wbs-v1_1.md +2183 -0
- salak-0.1.0/docs/salak-wbs-v1_2.md +2364 -0
- salak-0.1.0/docs/salak-wbs-v1_3.md +2399 -0
- salak-0.1.0/docs/salak-wbs-v1_4.md +2417 -0
- salak-0.1.0/docs/salak-wbs-v1_5.md +2551 -0
- salak-0.1.0/docs/schema-guide.md +420 -0
- salak-0.1.0/docs/troubleshooting.md +365 -0
- salak-0.1.0/package-lock.json +75 -0
- salak-0.1.0/package.json +13 -0
- salak-0.1.0/project-memory/00-master-plan.md +158 -0
- salak-0.1.0/project-memory/01-decision-register.md +446 -0
- salak-0.1.0/project-memory/02-gap-analysis.md +362 -0
- salak-0.1.0/project-memory/03-progress.md +7139 -0
- salak-0.1.0/project-memory/consumer-proxy-2026-08-01.md +86 -0
- salak-0.1.0/project-memory/precision-audit-2026-08-13.md +536 -0
- salak-0.1.0/project-memory/recall-baseline-2026-08-13.md +128 -0
- salak-0.1.0/project-memory/spike-0/README.md +98 -0
- salak-0.1.0/project-memory/spike-0/findings-python.md +282 -0
- salak-0.1.0/project-memory/spike-0/findings-size.md +263 -0
- salak-0.1.0/project-memory/spike-0/findings-typescript.md +274 -0
- salak-0.1.0/project-memory/spike-0/package-lock.json +73 -0
- salak-0.1.0/project-memory/spike-0/spike-0-findings.md +361 -0
- salak-0.1.0/project-memory/spike-0/versions.txt +70 -0
- salak-0.1.0/project-memory/v02-exit-gate-2026-08-13.md +121 -0
- salak-0.1.0/project-memory/v03-speedup-2026-08-22.md +58 -0
- salak-0.1.0/pyproject.toml +133 -0
- salak-0.1.0/src/salak/__init__.py +27 -0
- salak-0.1.0/src/salak/adapters/__init__.py +10 -0
- salak-0.1.0/src/salak/adapters/parse_recursion.py +45 -0
- salak-0.1.0/src/salak/adapters/python/__init__.py +382 -0
- salak-0.1.0/src/salak/adapters/python/imports.py +326 -0
- salak-0.1.0/src/salak/adapters/python/modules.py +649 -0
- salak-0.1.0/src/salak/adapters/python/sites.py +456 -0
- salak-0.1.0/src/salak/adapters/python/structure.py +528 -0
- salak-0.1.0/src/salak/adapters/typescript/__init__.py +384 -0
- salak-0.1.0/src/salak/adapters/typescript/imports.py +363 -0
- salak-0.1.0/src/salak/adapters/typescript/modules.py +603 -0
- salak-0.1.0/src/salak/adapters/typescript/sites.py +357 -0
- salak-0.1.0/src/salak/adapters/typescript/structure.py +416 -0
- salak-0.1.0/src/salak/adapters/typescript/tsconfig.py +438 -0
- salak-0.1.0/src/salak/check/__init__.py +27 -0
- salak-0.1.0/src/salak/check/exit_codes.py +32 -0
- salak-0.1.0/src/salak/check/staleness.py +94 -0
- salak-0.1.0/src/salak/cli/__init__.py +9 -0
- salak-0.1.0/src/salak/cli/check.py +74 -0
- salak-0.1.0/src/salak/cli/diff.py +44 -0
- salak-0.1.0/src/salak/cli/doctor.py +134 -0
- salak-0.1.0/src/salak/cli/exit_codes.py +46 -0
- salak-0.1.0/src/salak/cli/main.py +185 -0
- salak-0.1.0/src/salak/cli/scan.py +688 -0
- salak-0.1.0/src/salak/cli/validate.py +174 -0
- salak-0.1.0/src/salak/cli/version.py +43 -0
- salak-0.1.0/src/salak/core/__init__.py +6 -0
- salak-0.1.0/src/salak/core/adapter.py +190 -0
- salak-0.1.0/src/salak/core/discovery.py +444 -0
- salak-0.1.0/src/salak/core/emit.py +338 -0
- salak-0.1.0/src/salak/core/ignore.py +408 -0
- salak-0.1.0/src/salak/core/merge.py +217 -0
- salak-0.1.0/src/salak/core/models.py +651 -0
- salak-0.1.0/src/salak/core/node_id.py +172 -0
- salak-0.1.0/src/salak/core/paths.py +202 -0
- salak-0.1.0/src/salak/core/prior.py +529 -0
- salak-0.1.0/src/salak/core/registry.py +255 -0
- salak-0.1.0/src/salak/core/stats.py +219 -0
- salak-0.1.0/src/salak/core/syntax_error.py +116 -0
- salak-0.1.0/src/salak/core/write.py +162 -0
- salak-0.1.0/src/salak/diff/__init__.py +72 -0
- salak-0.1.0/src/salak/diff/compute.py +164 -0
- salak-0.1.0/src/salak/diff/exit_codes.py +41 -0
- salak-0.1.0/src/salak/diff/loading.py +150 -0
- salak-0.1.0/src/salak/diff/render.py +176 -0
- salak-0.1.0/src/salak/incremental/__init__.py +157 -0
- salak-0.1.0/src/salak/incremental/candidate_closure.py +78 -0
- salak-0.1.0/src/salak/incremental/carry_over.py +111 -0
- salak-0.1.0/src/salak/incremental/closure.py +208 -0
- salak-0.1.0/src/salak/incremental/compatibility.py +178 -0
- salak-0.1.0/src/salak/incremental/deletion.py +153 -0
- salak-0.1.0/src/salak/incremental/dirty.py +207 -0
- salak-0.1.0/src/salak/incremental/partial_pass1.py +84 -0
- salak-0.1.0/src/salak/incremental/partial_pass2.py +158 -0
- salak-0.1.0/src/salak/incremental/pipeline.py +263 -0
- salak-0.1.0/src/salak/incremental/recompute.py +109 -0
- salak-0.1.0/src/salak/incremental/reverse.py +179 -0
- salak-0.1.0/src/salak/incremental/safety_valve.py +127 -0
- salak-0.1.0/src/salak/incremental/symbol_delta.py +174 -0
- salak-0.1.0/src/salak/lsp/__init__.py +13 -0
- salak-0.1.0/src/salak/lsp/client.py +605 -0
- salak-0.1.0/src/salak/lsp/detect.py +357 -0
- salak-0.1.0/src/salak/lsp/readiness.py +316 -0
- salak-0.1.0/src/salak/precision/__init__.py +47 -0
- salak-0.1.0/src/salak/precision/__main__.py +71 -0
- salak-0.1.0/src/salak/precision/render.py +252 -0
- salak-0.1.0/src/salak/precision/sample.py +306 -0
- salak-0.1.0/src/salak/py.typed +0 -0
- salak-0.1.0/src/salak/resolution/__init__.py +75 -0
- salak-0.1.0/src/salak/resolution/edges.py +410 -0
- salak-0.1.0/src/salak/resolution/engine.py +267 -0
- salak-0.1.0/src/salak/resolution/models.py +137 -0
- salak-0.1.0/src/salak/resolution/provenance.py +292 -0
- salak-0.1.0/src/salak/resolution/unresolved.py +140 -0
- salak-0.1.0/src/salak/schema/__init__.py +120 -0
- salak-0.1.0/src/salak/schema/repo-graph-1.0.schema.json +621 -0
- salak-0.1.0/tests/acceptance_matrix.py +144 -0
- salak-0.1.0/tests/fixtures/README.md +67 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/barrel/index.ts +1 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/caller.ts +6 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/consumer.ts +5 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/depends_on_removable.ts +5 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/impl.ts +9 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/mystery.ts +5 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/removable.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/standalone.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_00.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_01.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_02.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_03.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_04.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_05.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_06.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_07.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_08.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_09.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_10.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_11.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_12.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_13.ts +3 -0
- salak-0.1.0/tests/fixtures/acceptance-matrix/src/unrelated_14.ts +3 -0
- salak-0.1.0/tests/fixtures/broken/EXPECTED.md +127 -0
- salak-0.1.0/tests/fixtures/broken/src/dispatch.ts +6 -0
- salak-0.1.0/tests/fixtures/broken/src/half_merged.py +10 -0
- salak-0.1.0/tests/fixtures/broken/src/inventory.ts +7 -0
- salak-0.1.0/tests/fixtures/broken/src/ledger.py +15 -0
- salak-0.1.0/tests/fixtures/broken/src/money.py +5 -0
- salak-0.1.0/tests/fixtures/broken/src/reporting.py +7 -0
- salak-0.1.0/tests/fixtures/broken/src/truncated.ts +7 -0
- salak-0.1.0/tests/fixtures/broken/src/units.ts +5 -0
- salak-0.1.0/tests/fixtures/graphs/corrupt-1.0.json +108 -0
- salak-0.1.0/tests/fixtures/graphs/no-schema-version.json +109 -0
- salak-0.1.0/tests/fixtures/graphs/not-an-object.json +1 -0
- salak-0.1.0/tests/fixtures/graphs/unknown-version.json +110 -0
- salak-0.1.0/tests/fixtures/graphs/valid-1.0.json +110 -0
- salak-0.1.0/tests/fixtures/py-basic/EXPECTED.md +146 -0
- salak-0.1.0/tests/fixtures/py-basic/app/__init__.py +1 -0
- salak-0.1.0/tests/fixtures/py-basic/app/models.py +15 -0
- salak-0.1.0/tests/fixtures/py-basic/app/service.py +13 -0
- salak-0.1.0/tests/fixtures/py-basic/pyproject.toml +8 -0
- salak-0.1.0/tests/fixtures/py-sites/EXPECTED.md +87 -0
- salak-0.1.0/tests/fixtures/py-sites/src/__init__.py +6 -0
- salak-0.1.0/tests/fixtures/py-sites/src/kinds.py +28 -0
- salak-0.1.0/tests/fixtures/py-sites/src/service.py +28 -0
- salak-0.1.0/tests/fixtures/ts-basic/EXPECTED.md +115 -0
- salak-0.1.0/tests/fixtures/ts-basic/package.json +10 -0
- salak-0.1.0/tests/fixtures/ts-basic/src/billing.ts +16 -0
- salak-0.1.0/tests/fixtures/ts-basic/src/types.ts +8 -0
- salak-0.1.0/tests/fixtures/ts-basic/tsconfig.json +10 -0
- salak-0.1.0/tests/fixtures/ts-resolution/EXPECTED.md +172 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/aliases.ts +10 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/chained/deep/impl.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/chained/deep/index.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/chained/index.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/competing/index.ts +2 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/competing/one.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/competing/two.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/namespaced/impl.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/namespaced/index.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/renamed/impl.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/renamed/index.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/star/impl.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels/star/index.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/barrels.ts +10 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/dual-a/thing.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/dual-b/thing.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/feature/impl.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/feature/index.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/money.ts +3 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/ui/button.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/src/utils/format.ts +1 -0
- salak-0.1.0/tests/fixtures/ts-resolution/tsconfig.json +12 -0
- salak-0.1.0/tests/fixtures/ts-sites/EXPECTED.md +104 -0
- salak-0.1.0/tests/fixtures/ts-sites/src/kinds.ts +14 -0
- salak-0.1.0/tests/fixtures/ts-sites/src/service.ts +29 -0
- salak-0.1.0/tests/test_adapter_protocol.py +123 -0
- salak-0.1.0/tests/test_adapter_registry.py +314 -0
- salak-0.1.0/tests/test_alias_barrel_v02.py +343 -0
- salak-0.1.0/tests/test_candidate_closure.py +262 -0
- salak-0.1.0/tests/test_carry_over.py +252 -0
- salak-0.1.0/tests/test_check_staleness.py +128 -0
- salak-0.1.0/tests/test_cli_check.py +104 -0
- salak-0.1.0/tests/test_cli_diff.py +111 -0
- salak-0.1.0/tests/test_cli_doctor.py +142 -0
- salak-0.1.0/tests/test_cli_help.py +40 -0
- salak-0.1.0/tests/test_cli_scan.py +419 -0
- salak-0.1.0/tests/test_cli_validate.py +254 -0
- salak-0.1.0/tests/test_cli_version.py +58 -0
- salak-0.1.0/tests/test_closure.py +319 -0
- salak-0.1.0/tests/test_compatibility_guard.py +307 -0
- salak-0.1.0/tests/test_context_pack.py +114 -0
- salak-0.1.0/tests/test_core_imports_no_adapter.py +163 -0
- salak-0.1.0/tests/test_degradation.py +318 -0
- salak-0.1.0/tests/test_degraded_mode.py +255 -0
- salak-0.1.0/tests/test_deletion.py +243 -0
- salak-0.1.0/tests/test_determinism.py +602 -0
- salak-0.1.0/tests/test_determinism_pass2.py +350 -0
- salak-0.1.0/tests/test_diff_compute.py +209 -0
- salak-0.1.0/tests/test_diff_ground_truth.py +228 -0
- salak-0.1.0/tests/test_diff_loading.py +210 -0
- salak-0.1.0/tests/test_diff_render.py +184 -0
- salak-0.1.0/tests/test_dirty_set.py +368 -0
- salak-0.1.0/tests/test_discovery.py +405 -0
- salak-0.1.0/tests/test_emit.py +493 -0
- salak-0.1.0/tests/test_fixtures.py +175 -0
- salak-0.1.0/tests/test_git_friendliness.py +469 -0
- salak-0.1.0/tests/test_ignore_rules.py +511 -0
- salak-0.1.0/tests/test_inheritance.py +388 -0
- salak-0.1.0/tests/test_instantiates.py +394 -0
- salak-0.1.0/tests/test_integration_reverification.py +293 -0
- salak-0.1.0/tests/test_lsp_client.py +345 -0
- salak-0.1.0/tests/test_merge.py +354 -0
- salak-0.1.0/tests/test_node_id.py +272 -0
- salak-0.1.0/tests/test_overwrite_guard.py +734 -0
- salak-0.1.0/tests/test_packaging_metadata.py +69 -0
- salak-0.1.0/tests/test_partial_pass1.py +210 -0
- salak-0.1.0/tests/test_partial_pass2.py +297 -0
- salak-0.1.0/tests/test_path_containment.py +441 -0
- salak-0.1.0/tests/test_precision_sample.py +422 -0
- salak-0.1.0/tests/test_provenance.py +413 -0
- salak-0.1.0/tests/test_python_adapter.py +314 -0
- salak-0.1.0/tests/test_python_external.py +354 -0
- salak-0.1.0/tests/test_python_imports.py +619 -0
- salak-0.1.0/tests/test_python_sites.py +458 -0
- salak-0.1.0/tests/test_python_structure.py +472 -0
- salak-0.1.0/tests/test_readiness.py +445 -0
- salak-0.1.0/tests/test_recompute.py +200 -0
- salak-0.1.0/tests/test_references.py +494 -0
- salak-0.1.0/tests/test_resilience.py +373 -0
- salak-0.1.0/tests/test_resolution.py +650 -0
- salak-0.1.0/tests/test_resolution_fixture.py +290 -0
- salak-0.1.0/tests/test_resource_bounds.py +339 -0
- salak-0.1.0/tests/test_reverse_index.py +329 -0
- salak-0.1.0/tests/test_safety_valve.py +130 -0
- salak-0.1.0/tests/test_schema_bundling.py +209 -0
- salak-0.1.0/tests/test_schema_envelope.py +283 -0
- salak-0.1.0/tests/test_schema_records.py +443 -0
- salak-0.1.0/tests/test_server_detection.py +388 -0
- salak-0.1.0/tests/test_stats.py +262 -0
- salak-0.1.0/tests/test_subprocess_hardening.py +468 -0
- salak-0.1.0/tests/test_symbol_delta.py +350 -0
- salak-0.1.0/tests/test_syntax_error.py +173 -0
- salak-0.1.0/tests/test_typescript_adapter.py +243 -0
- salak-0.1.0/tests/test_typescript_external.py +399 -0
- salak-0.1.0/tests/test_typescript_imports.py +583 -0
- salak-0.1.0/tests/test_typescript_sites.py +438 -0
- salak-0.1.0/tests/test_typescript_structure.py +390 -0
- salak-0.1.0/tests/test_typescript_tsconfig.py +344 -0
- salak-0.1.0/tests/test_unresolved.py +443 -0
- salak-0.1.0/tests/test_untrusted_resolver_output.py +155 -0
- salak-0.1.0/tests/test_v03_acceptance_matrix.py +206 -0
- salak-0.1.0/tests/test_v03_incremental_equals_full.py +273 -0
- salak-0.1.0/tests/test_wheel_installation.py +99 -0
- salak-0.1.0/tests/test_write_path_safety.py +233 -0
- salak-0.1.0/uv.lock +563 -0
salak-0.1.0/.cursorrules
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Salak — deterministic code knowledge graph generator. Python 3.11+, PyPI `salak`.
|
|
2
|
+
Parses a repo, emits one provenance-tagged JSON file (`repo-graph.json`).
|
|
3
|
+
It is a BUILD ARTIFACT, not a database. Built to unblock Jeruk AI/MANGGA, not to
|
|
4
|
+
become a product.
|
|
5
|
+
|
|
6
|
+
READ THESE TWO FILES BEFORE DOING ANYTHING:
|
|
7
|
+
1. CLAUDE.md — working rules, the binding documents by name,
|
|
8
|
+
and what to do next
|
|
9
|
+
2. project-memory/03-progress.md — authoritative state, newest snapshot at the top
|
|
10
|
+
|
|
11
|
+
THIS FILE DELIBERATELY CARRIES NO PROJECT STATE.
|
|
12
|
+
No task numbers, no phase status, no document version numbers, no "next task".
|
|
13
|
+
It used to carry all four, and it went roughly thirty tasks stale without anyone
|
|
14
|
+
noticing — it still named a superseded spec and a task finished weeks earlier.
|
|
15
|
+
Five documents claimed the same facts and one of them was being maintained. So
|
|
16
|
+
the duplication is gone rather than merely corrected, and a test enforces it
|
|
17
|
+
(tests/test_context_pack.py). If you are tempted to paste status here, put it in
|
|
18
|
+
03-progress.md and let this file keep pointing at it.
|
|
19
|
+
|
|
20
|
+
DURABLE RULES — these do not change from task to task:
|
|
21
|
+
|
|
22
|
+
- Precision over recall. A wrong edge is worse than a missing edge.
|
|
23
|
+
- Provenance downgrades, never upgrades. `extracted` means the relation is
|
|
24
|
+
literally in source AND the target was confirmed — guess any part of the target
|
|
25
|
+
and it is not `extracted`.
|
|
26
|
+
- Determinism is a test, not an aspiration. Two runs over an unchanged tree are
|
|
27
|
+
byte-identical except `generated_at`. Never let `set` or `dict` iteration order
|
|
28
|
+
reach the output.
|
|
29
|
+
- Core must never import an adapter. Registration goes through the
|
|
30
|
+
`salak.adapters` entry-point group, and a test enforces the direction.
|
|
31
|
+
- Every path in the artifact is repo-relative with POSIX separators. This repo is
|
|
32
|
+
developed on Windows; assume nothing.
|
|
33
|
+
- Degraded is not failed. No language server means structure plus import edges,
|
|
34
|
+
not an error. `health_check()` never raises; `resolve()` may return empty.
|
|
35
|
+
- Diagnostics never abort a run. A broken file yields a diagnostic and the scan
|
|
36
|
+
completes.
|
|
37
|
+
- Exit codes are a public contract: 0 clean · 1 fatal · 2 completed with
|
|
38
|
+
diagnostics · 3 overwrite guard refused.
|
|
39
|
+
- The bundled JSON Schema beats the prose. If the two disagree, the schema is
|
|
40
|
+
right and the prose gets fixed.
|
|
41
|
+
- No placeholders. No "implement later", no stub returning a fake value. If a task
|
|
42
|
+
is too large to finish, say so and split it before starting.
|
|
43
|
+
- Any binding decision the spec does not already make: STOP and ask the owner,
|
|
44
|
+
then record the approved answer in project-memory/01-decision-register.md.
|
|
45
|
+
- One task at a time, in the plan's order — not numeric order; the two disagree,
|
|
46
|
+
and following the numbers has already caused a task to be skipped. Check
|
|
47
|
+
dependencies before starting.
|
|
48
|
+
- Gates are hard. A `rework` verdict invalidates the downstream task list rather
|
|
49
|
+
than delaying it. Never pull later-phase work forward.
|
|
50
|
+
- Run the real tools. Never describe output you did not see.
|
|
51
|
+
- Update project-memory/03-progress.md in the same turn as the work it records.
|
|
52
|
+
|
|
53
|
+
HARD BOUNDARIES — decisions, not preferences. Do not build, scaffold, or
|
|
54
|
+
"prepare for": a UI or graph viewer · document/PDF/image ingestion · any LLM
|
|
55
|
+
call · clustering or community detection · cross-repo graphs · any language
|
|
56
|
+
beyond TypeScript and Python · dev-kickoff integration · `salak query` · watch
|
|
57
|
+
mode · metrics or complexity scoring. Touching one of these is an anti-pattern,
|
|
58
|
+
not initiative. Name it out loud instead.
|
|
59
|
+
|
|
60
|
+
VERIFY THE REPO:
|
|
61
|
+
uv sync && uv run ruff check . && uv run mypy src tests && uv run pytest
|
|
62
|
+
|
|
63
|
+
CONVENTIONS: `src/salak/` layout · ruff + mypy strict · CI on 3.11 and 3.12 ·
|
|
64
|
+
snake_case functions, PascalCase classes, SCREAMING_SNAKE_CASE diagnostic codes ·
|
|
65
|
+
full type hints · `LanguageAdapter` is a `typing.Protocol` · Conventional Commits
|
|
66
|
+
with the phase as scope, e.g. `feat(v0.1): emit files section`.
|
|
67
|
+
|
|
68
|
+
Talk to the owner in Bahasa Indonesia. Write every file, comment, commit message
|
|
69
|
+
and document in English.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# LF everywhere, on every platform.
|
|
2
|
+
#
|
|
3
|
+
# This repository is developed on Windows but its output is a build artifact whose
|
|
4
|
+
# whole point is byte-identical, file-scoped diffs (SDD §3.6, acceptance criterion 6).
|
|
5
|
+
# Mixed line endings make every diff noisy and would eventually be mistaken for a
|
|
6
|
+
# determinism defect. Normalised repo-wide at TASK-016 after 12 tracked files had
|
|
7
|
+
# drifted to CRLF.
|
|
8
|
+
* text=auto eol=lf
|
|
9
|
+
|
|
10
|
+
# Keep the artifact's own rules explicit — it is JSON, emitted one record per line.
|
|
11
|
+
*.json text eol=lf
|
|
12
|
+
repo-graph.json text eol=lf
|
|
13
|
+
|
|
14
|
+
*.png binary
|
|
15
|
+
*.jpg binary
|
|
16
|
+
*.ico binary
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copilot instructions — Salak
|
|
2
|
+
|
|
3
|
+
Salak — deterministic code knowledge graph generator. Python 3.11+, published on
|
|
4
|
+
PyPI as `salak`. It parses a repository and emits one provenance-tagged JSON file
|
|
5
|
+
(`repo-graph.json`) so an assistant can answer *"what depends on this module?"*
|
|
6
|
+
from a recorded fact instead of a guess. The graph is a **build artifact, not a
|
|
7
|
+
database**. It exists to unblock Jeruk AI and MANGGA; it is not meant to become a
|
|
8
|
+
product.
|
|
9
|
+
|
|
10
|
+
## Read these two files before doing anything
|
|
11
|
+
|
|
12
|
+
| File | What it holds |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `CLAUDE.md` | The working rules, the binding documents **by name**, and what to do next |
|
|
15
|
+
| `project-memory/03-progress.md` | Authoritative state — newest snapshot at the top |
|
|
16
|
+
|
|
17
|
+
## This file deliberately carries no project state
|
|
18
|
+
|
|
19
|
+
No task numbers, no phase status, no document version numbers, no "next task".
|
|
20
|
+
|
|
21
|
+
It used to carry all four, and it went roughly thirty tasks stale without anyone
|
|
22
|
+
noticing — still naming a superseded spec and a task finished weeks earlier. The
|
|
23
|
+
cause was structural rather than careless: five documents claimed the same facts
|
|
24
|
+
and only one of them was being maintained. So the duplication is gone rather than
|
|
25
|
+
merely corrected, and a test enforces it (`tests/test_context_pack.py`).
|
|
26
|
+
|
|
27
|
+
If you are tempted to paste status here, put it in `03-progress.md` and let this
|
|
28
|
+
file keep pointing at it.
|
|
29
|
+
|
|
30
|
+
## Durable rules — these do not change from task to task
|
|
31
|
+
|
|
32
|
+
- **Precision over recall.** A wrong edge is worse than a missing edge.
|
|
33
|
+
- **Provenance downgrades, never upgrades.** `extracted` means the relation is
|
|
34
|
+
literally in source **and** the target was confirmed. Guess any part of the
|
|
35
|
+
target and it is not `extracted`.
|
|
36
|
+
- **Determinism is a test.** Two runs over an unchanged tree are byte-identical
|
|
37
|
+
except `generated_at`. Never let a `set` or `dict` iteration order reach the
|
|
38
|
+
output.
|
|
39
|
+
- **Core must never import an adapter.** Registration goes through the
|
|
40
|
+
`salak.adapters` entry-point group, and a test enforces the direction.
|
|
41
|
+
- **Every path in the artifact is repo-relative with POSIX separators.** This
|
|
42
|
+
repository is developed on Windows; assume nothing.
|
|
43
|
+
- **Degraded is not failed.** No language server means structure plus import
|
|
44
|
+
edges, not an error. `health_check()` never raises.
|
|
45
|
+
- **Diagnostics never abort a run.** A broken file yields a diagnostic and the
|
|
46
|
+
scan completes.
|
|
47
|
+
- **Exit codes are a public contract:** `0` clean · `1` fatal · `2` completed with
|
|
48
|
+
diagnostics · `3` overwrite guard refused.
|
|
49
|
+
- **The bundled JSON Schema beats the prose.** If they disagree, the schema is
|
|
50
|
+
right and the prose gets fixed.
|
|
51
|
+
- **No placeholders.** No "implement later", no stub returning a fake value. If a
|
|
52
|
+
task is too large to finish, say so and split it before starting.
|
|
53
|
+
- **Any binding decision the spec does not make: STOP and ask the owner**, then
|
|
54
|
+
record the approved answer in `project-memory/01-decision-register.md`.
|
|
55
|
+
- **One task at a time, in the plan's order** — not numeric order; the two
|
|
56
|
+
disagree, and following the numbers has already caused a task to be skipped.
|
|
57
|
+
- **Gates are hard.** A `rework` verdict invalidates the downstream task list
|
|
58
|
+
rather than delaying it. Never pull later-phase work forward.
|
|
59
|
+
- **Run the real tools.** Never describe output you did not see.
|
|
60
|
+
|
|
61
|
+
## Hard boundaries — decisions, not preferences
|
|
62
|
+
|
|
63
|
+
Do not build, scaffold, or "prepare for": a UI or graph viewer · document, PDF or
|
|
64
|
+
image ingestion · **any LLM call** · clustering or community detection ·
|
|
65
|
+
cross-repo graphs · any language beyond TypeScript and Python · dev-kickoff
|
|
66
|
+
integration · **`salak query`** · watch mode · metrics or complexity scoring.
|
|
67
|
+
|
|
68
|
+
Touching one of these is an anti-pattern, not initiative. Name it out loud instead.
|
|
69
|
+
|
|
70
|
+
## Verify the repo
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv sync && uv run ruff check . && uv run mypy src tests && uv run pytest
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Conventions
|
|
77
|
+
|
|
78
|
+
`src/salak/` layout · ruff + mypy strict · CI on 3.11 and 3.12 · `snake_case`
|
|
79
|
+
functions · `PascalCase` classes · `SCREAMING_SNAKE_CASE` diagnostic codes · full
|
|
80
|
+
type hints · `LanguageAdapter` is a `typing.Protocol` · Conventional Commits with
|
|
81
|
+
the phase as scope, e.g. `feat(v0.1): emit files section`.
|
|
82
|
+
|
|
83
|
+
Talk to the owner in **Bahasa Indonesia**. Write every file, comment, commit
|
|
84
|
+
message and document in **English**.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# TASK-009 — lint, type-check and tests on Python 3.11 and 3.12.
|
|
2
|
+
#
|
|
3
|
+
# Three things here are deliberate rather than boilerplate:
|
|
4
|
+
#
|
|
5
|
+
# * **The steps are ordered and none carries `continue-on-error`.** A failing `ruff`
|
|
6
|
+
# or `mypy` fails the job and stops it before the tests run, which is DoD item 3.
|
|
7
|
+
# Nothing here may be softened into a warning.
|
|
8
|
+
# * **mypy is given the matrix version explicitly.** `pyproject.toml` pins
|
|
9
|
+
# `python_version = "3.11"`, so without `--python-version` the 3.12 leg would run the
|
|
10
|
+
# *same* type-check as the 3.11 leg and the matrix would only be varying the pytest
|
|
11
|
+
# runtime. The CLI flag overrides the config, so both legs genuinely differ.
|
|
12
|
+
# * **`pytest` exit code 5 is accepted, loudly.** See the test step.
|
|
13
|
+
|
|
14
|
+
name: CI
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
push:
|
|
18
|
+
branches: [main]
|
|
19
|
+
pull_request:
|
|
20
|
+
branches: [main]
|
|
21
|
+
|
|
22
|
+
# Read-only by default. The security track (P5, TASK-099…102) treats an
|
|
23
|
+
# over-permissioned workflow token as a finding, and this is the cheap moment to
|
|
24
|
+
# get it right.
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
|
|
28
|
+
concurrency:
|
|
29
|
+
group: ci-${{ github.ref }}
|
|
30
|
+
cancel-in-progress: true
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
check:
|
|
34
|
+
name: py${{ matrix.python-version }}
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
strategy:
|
|
37
|
+
# Both legs always report. Cancelling 3.12 because 3.11 failed hides
|
|
38
|
+
# version-specific breakage, which is the only thing a matrix exists to find.
|
|
39
|
+
fail-fast: false
|
|
40
|
+
matrix:
|
|
41
|
+
python-version: ["3.11", "3.12"]
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
|
|
46
|
+
- name: Install uv
|
|
47
|
+
uses: astral-sh/setup-uv@v5
|
|
48
|
+
with:
|
|
49
|
+
enable-cache: true
|
|
50
|
+
|
|
51
|
+
- name: Sync dependencies
|
|
52
|
+
# `--frozen` refuses to update uv.lock. A pyproject.toml edited without
|
|
53
|
+
# re-locking then fails here rather than silently resolving to something no
|
|
54
|
+
# developer has ever run.
|
|
55
|
+
run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
56
|
+
|
|
57
|
+
- name: Install Node
|
|
58
|
+
# Phase 2 is entirely about language servers, so a CI that cannot run them cannot
|
|
59
|
+
# verify the phase at all. `npm ci` installs the exact pins in package.json —
|
|
60
|
+
# pyright, typescript-language-server, and typescript 5.9.3 as a CEILING (D15).
|
|
61
|
+
# The postinstall hook fails the job if `typescript/lib/tsserver.js` is missing,
|
|
62
|
+
# which is the whole of that ceiling's compatibility test.
|
|
63
|
+
uses: actions/setup-node@v4
|
|
64
|
+
with:
|
|
65
|
+
node-version: "24"
|
|
66
|
+
|
|
67
|
+
- name: Install language servers (npm ci)
|
|
68
|
+
run: npm ci --no-audit --no-fund
|
|
69
|
+
|
|
70
|
+
- name: Lint (ruff)
|
|
71
|
+
run: uv run ruff check .
|
|
72
|
+
|
|
73
|
+
- name: Type-check (mypy, strict, src + tests)
|
|
74
|
+
run: uv run mypy --python-version ${{ matrix.python-version }}
|
|
75
|
+
|
|
76
|
+
- name: Test (pytest)
|
|
77
|
+
shell: bash
|
|
78
|
+
# `pytest` exits **5** when it collects no tests, and an exit-5 CI step is red.
|
|
79
|
+
# TASK-009's Definition of Done is literally "CI green on an empty test suite",
|
|
80
|
+
# so the condition is handled explicitly instead of being left to depend on the
|
|
81
|
+
# suite happening to be non-empty (gap analysis G1).
|
|
82
|
+
#
|
|
83
|
+
# Exit 5 means specifically "no tests collected" — a collection *error* exits 2
|
|
84
|
+
# or 3 and still fails the job — so this cannot mask a broken test module. It
|
|
85
|
+
# can mask a mis-set `testpaths`, which is why it annotates the run instead of
|
|
86
|
+
# passing in silence.
|
|
87
|
+
run: |
|
|
88
|
+
set +e
|
|
89
|
+
uv run pytest
|
|
90
|
+
code=$?
|
|
91
|
+
set -e
|
|
92
|
+
if [ "$code" -eq 5 ]; then
|
|
93
|
+
echo "::warning title=Empty test suite::pytest collected no tests (exit 5). Treated as a pass per TASK-009 DoD, but a non-empty suite is expected from TASK-010 onward."
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
exit "$code"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# TASK-095 — clean-machine installation acceptance test. SDD §6 v1.0 done criterion.
|
|
2
|
+
#
|
|
3
|
+
# §2.1 requires two Node-based language servers that PyPI cannot install, so
|
|
4
|
+
# "installable on a clean machine" is satisfiable in a state where those servers are
|
|
5
|
+
# absent entirely — this task exists to force that gap into the open rather than let
|
|
6
|
+
# "clean machine" mean whatever the person running the check happens to have installed.
|
|
7
|
+
#
|
|
8
|
+
# **D54 (project-memory/01-decision-register.md): "clean machine" is defined as three
|
|
9
|
+
# states, all three required to pass, none more authoritative than another:**
|
|
10
|
+
# 1. No Node.js at all
|
|
11
|
+
# 2. Node.js present, no language server installed
|
|
12
|
+
# 3. Node.js present, both pinned language servers installed (`npm ci`, same shape
|
|
13
|
+
# `ci.yml` already uses)
|
|
14
|
+
#
|
|
15
|
+
# **What "capability difference" means today, proven rather than asserted**: after
|
|
16
|
+
# K15/D43, no module under `salak.resolution` imports `salak.lsp`, so a language
|
|
17
|
+
# server's presence changes nothing about what a scan emits. The final step proves
|
|
18
|
+
# this concretely — all three states scan the identical fixture and their graphs must
|
|
19
|
+
# be byte-identical except `generated_at`, checked with `salak diff` itself.
|
|
20
|
+
#
|
|
21
|
+
# **One job, not three-plus-a-comparison.** The first version of this workflow used
|
|
22
|
+
# four jobs (one per state, one to compare) and passed graphs between them via
|
|
23
|
+
# `actions/upload-artifact`/`download-artifact`. Real run, 2026-08-26: every state's
|
|
24
|
+
# own `doctor`/`scan` step passed, but every `upload-artifact` step failed —
|
|
25
|
+
# "Artifact storage quota has been hit" — an account-level GitHub Actions storage
|
|
26
|
+
# limit, not a bug in the check itself. Node's presence and installed servers are
|
|
27
|
+
# runtime, not install-time, facts, so all three states can be exercised sequentially
|
|
28
|
+
# against **one** build and **one** install in **one** container's filesystem, with the
|
|
29
|
+
# three graphs compared as local files — no artifact transfer needed at all.
|
|
30
|
+
|
|
31
|
+
name: Clean-machine matrix
|
|
32
|
+
|
|
33
|
+
on:
|
|
34
|
+
push:
|
|
35
|
+
branches: [main]
|
|
36
|
+
pull_request:
|
|
37
|
+
branches: [main]
|
|
38
|
+
|
|
39
|
+
permissions:
|
|
40
|
+
contents: read
|
|
41
|
+
|
|
42
|
+
concurrency:
|
|
43
|
+
group: clean-machine-${{ github.ref }}
|
|
44
|
+
cancel-in-progress: true
|
|
45
|
+
|
|
46
|
+
jobs:
|
|
47
|
+
clean-machine:
|
|
48
|
+
name: Three states, one container, no artifacts
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
# Debian-based, so Node can be installed with apt partway through — and, until
|
|
51
|
+
# that step runs, genuinely has no Node anywhere on the image (checked, not
|
|
52
|
+
# trusted, in "State 1" below).
|
|
53
|
+
container: python:3.11-slim
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/checkout@v4
|
|
56
|
+
|
|
57
|
+
- name: Install uv
|
|
58
|
+
run: pip install --no-cache-dir uv
|
|
59
|
+
|
|
60
|
+
- name: Sync and build once — every state below installs the same wheel
|
|
61
|
+
run: |
|
|
62
|
+
uv sync --frozen --python 3.11
|
|
63
|
+
uv build
|
|
64
|
+
|
|
65
|
+
- name: Install salak from the built wheel into a fresh venv
|
|
66
|
+
run: |
|
|
67
|
+
uv venv /tmp/clean-venv --python 3.11
|
|
68
|
+
uv pip install --python /tmp/clean-venv/bin/python dist/*.whl
|
|
69
|
+
|
|
70
|
+
- name: Make a tiny fixture, scanned identically in every state below
|
|
71
|
+
run: |
|
|
72
|
+
mkdir -p /tmp/fixture
|
|
73
|
+
printf 'export const x = 1;\n' > /tmp/fixture/a.ts
|
|
74
|
+
|
|
75
|
+
# ----------------------------------------------------------------- State 1
|
|
76
|
+
|
|
77
|
+
- name: "State 1: confirm no Node, then doctor + scan"
|
|
78
|
+
run: |
|
|
79
|
+
if command -v node >/dev/null 2>&1; then
|
|
80
|
+
echo "::error::this image already has Node on PATH; State 1 is not testing what it claims to"
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
/tmp/clean-venv/bin/salak doctor | tee doctor-1.txt
|
|
84
|
+
grep -q "Node.js: NOT FOUND" doctor-1.txt
|
|
85
|
+
grep -q "python: ok" doctor-1.txt
|
|
86
|
+
grep -q "typescript: ok" doctor-1.txt
|
|
87
|
+
/tmp/clean-venv/bin/salak scan /tmp/fixture --out "$PWD/graph-1-no-node.json" --no-lsp
|
|
88
|
+
|
|
89
|
+
# ----------------------------------------------------------------- State 2
|
|
90
|
+
|
|
91
|
+
- name: Install Node (no language servers yet)
|
|
92
|
+
run: |
|
|
93
|
+
apt-get update -qq
|
|
94
|
+
apt-get install -y --no-install-recommends nodejs npm >/dev/null
|
|
95
|
+
|
|
96
|
+
- name: "State 2: Node present, no servers — doctor + scan"
|
|
97
|
+
run: |
|
|
98
|
+
command -v node >/dev/null 2>&1 || { echo "::error::Node install did not put node on PATH"; exit 1; }
|
|
99
|
+
/tmp/clean-venv/bin/salak doctor | tee doctor-2.txt
|
|
100
|
+
grep -q "Node.js: found" doctor-2.txt
|
|
101
|
+
grep -q "pyright: not detected" doctor-2.txt
|
|
102
|
+
grep -q "typescript-language-server: not detected" doctor-2.txt
|
|
103
|
+
grep -q "python: ok" doctor-2.txt
|
|
104
|
+
grep -q "typescript: ok" doctor-2.txt
|
|
105
|
+
/tmp/clean-venv/bin/salak scan /tmp/fixture --out "$PWD/graph-2-node-no-servers.json" --no-lsp
|
|
106
|
+
|
|
107
|
+
# ----------------------------------------------------------------- State 3
|
|
108
|
+
|
|
109
|
+
- name: Install language servers (npm ci)
|
|
110
|
+
run: npm ci --no-audit --no-fund
|
|
111
|
+
|
|
112
|
+
- name: "State 3: Node present, both servers — doctor + scan"
|
|
113
|
+
run: |
|
|
114
|
+
/tmp/clean-venv/bin/salak doctor | tee doctor-3.txt
|
|
115
|
+
grep -q "Node.js: found" doctor-3.txt
|
|
116
|
+
grep -q "pyright " doctor-3.txt
|
|
117
|
+
grep -q "typescript-language-server:" doctor-3.txt
|
|
118
|
+
grep -q "python: ok" doctor-3.txt
|
|
119
|
+
grep -q "typescript: ok" doctor-3.txt
|
|
120
|
+
/tmp/clean-venv/bin/salak scan /tmp/fixture --out "$PWD/graph-3-node-with-servers.json" --no-lsp
|
|
121
|
+
|
|
122
|
+
# -------------------------------------------------------- Prove parity
|
|
123
|
+
|
|
124
|
+
- name: All three states must produce an identical graph
|
|
125
|
+
# The tool verifying its own DoD claim: `salak diff` already ignores
|
|
126
|
+
# `generated_at` (its whole reason to exist), so "empty" here means
|
|
127
|
+
# genuinely identical, not merely eyeballed-similar.
|
|
128
|
+
run: |
|
|
129
|
+
set -e
|
|
130
|
+
for pair in \
|
|
131
|
+
"graph-1-no-node.json graph-2-node-no-servers.json" \
|
|
132
|
+
"graph-1-no-node.json graph-3-node-with-servers.json" \
|
|
133
|
+
"graph-2-node-no-servers.json graph-3-node-with-servers.json"; do
|
|
134
|
+
echo "::group::diff $pair"
|
|
135
|
+
/tmp/clean-venv/bin/salak diff $pair
|
|
136
|
+
echo "::endgroup::"
|
|
137
|
+
done
|
|
138
|
+
echo "All three clean-machine states produced an identical graph."
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# TASK-086 — TestPyPI publish rehearsal. SDD §5, WBS §4.1.
|
|
2
|
+
#
|
|
3
|
+
# Manually triggered (`workflow_dispatch`), not tag-triggered — a TestPyPI rehearsal is
|
|
4
|
+
# something the owner runs deliberately before cutting a real release, as many times as
|
|
5
|
+
# needed, without it consuming a real version tag. Give it whatever version string you
|
|
6
|
+
# want to test with; TestPyPI's index is separate from PyPI's, so nothing here can
|
|
7
|
+
# collide with a real published version.
|
|
8
|
+
#
|
|
9
|
+
# Requires the same one-time PyPI Trusted Publishing setup as `release.yml`, done on
|
|
10
|
+
# **test.pypi.org** specifically (a separate account/project registration from the real
|
|
11
|
+
# pypi.org one) — see `docs/RELEASING.md`.
|
|
12
|
+
|
|
13
|
+
name: Publish to TestPyPI
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
version:
|
|
19
|
+
description: "Version string to build and publish (e.g. 0.1.0.dev1) — TestPyPI only, does not need to match any git tag"
|
|
20
|
+
required: true
|
|
21
|
+
type: string
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
publish-testpypi:
|
|
25
|
+
name: Build and publish to TestPyPI
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
environment: testpypi
|
|
28
|
+
# Job-level `permissions:` REPLACES the workflow-level block for this job, it does
|
|
29
|
+
# not merge with it — a real bug found running this workflow for the first time
|
|
30
|
+
# (2026-08-26): `contents: read` lived only at the workflow level, so the job's
|
|
31
|
+
# own `permissions: {id-token: write}` silently dropped it, and actions/checkout
|
|
32
|
+
# got a token with no read access to the repo at all ("repository ... not found").
|
|
33
|
+
# Both permissions this job actually needs are listed here, explicitly, together.
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
id-token: write
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- name: Rewrite version for this rehearsal
|
|
42
|
+
# Not committed back, same as release.yml — this build never needs to match
|
|
43
|
+
# anything the repository's own history records.
|
|
44
|
+
run: |
|
|
45
|
+
version="${{ inputs.version }}"
|
|
46
|
+
sed -i "s/^version = \".*\"/version = \"${version}\"/" pyproject.toml
|
|
47
|
+
sed -i "s/^__version__ = \".*\"/__version__ = \"${version}\"/" src/salak/__init__.py
|
|
48
|
+
grep -q "version = \"${version}\"" pyproject.toml
|
|
49
|
+
grep -q "__version__ = \"${version}\"" src/salak/__init__.py
|
|
50
|
+
|
|
51
|
+
- name: Install uv
|
|
52
|
+
uses: astral-sh/setup-uv@v5
|
|
53
|
+
with:
|
|
54
|
+
enable-cache: true
|
|
55
|
+
|
|
56
|
+
- name: Sync dependencies
|
|
57
|
+
run: uv sync --python "3.11"
|
|
58
|
+
|
|
59
|
+
- name: Verify (lint, type-check, packaging + wheel-install tests)
|
|
60
|
+
run: |
|
|
61
|
+
uv run ruff check .
|
|
62
|
+
uv run mypy src tests
|
|
63
|
+
uv run pytest tests/test_cli_version.py tests/test_packaging_metadata.py tests/test_wheel_installation.py -q
|
|
64
|
+
|
|
65
|
+
- name: Build
|
|
66
|
+
run: uv build
|
|
67
|
+
|
|
68
|
+
- name: Publish to TestPyPI
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
with:
|
|
71
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# TASK-087 — release automation from a git tag. SDD §5, WBS Sec4.1.
|
|
2
|
+
#
|
|
3
|
+
# Triggered by pushing a tag matching `v*` (e.g. `v1.0.0`). Three things here are
|
|
4
|
+
# deliberate rather than boilerplate:
|
|
5
|
+
#
|
|
6
|
+
# * **The version is derived from the tag, not typed twice.** `pyproject.toml` and
|
|
7
|
+
# `src/salak/__init__.py` both carry a version string
|
|
8
|
+
# (`tests/test_cli_version.py::test_package_version_matches_pyprojects_declared_version`
|
|
9
|
+
# already guards the two staying equal to *each other*); this job additionally makes
|
|
10
|
+
# *the tag itself* the source of truth by rewriting both to match it before building,
|
|
11
|
+
# so "version string matches the release tag" (TASK-087 DoD item 3) holds by
|
|
12
|
+
# construction rather than by someone remembering to bump two files before tagging.
|
|
13
|
+
# D53 records why this is a workflow-time rewrite rather than adopting a VCS-derived
|
|
14
|
+
# dynamic-versioning build backend (`hatch-vcs`) — see `project-memory/01-decision-register.md`.
|
|
15
|
+
# * **Full verification runs against the *retagged* tree**, not the tree as committed —
|
|
16
|
+
# a version string is part of what ships, and a bug the bump script itself introduces
|
|
17
|
+
# (a malformed version, a partial rewrite) must fail the job before anything is
|
|
18
|
+
# published, the same way a broken feature would.
|
|
19
|
+
# * **Publishing uses PyPI Trusted Publishing (OIDC)**, not a stored API token. No
|
|
20
|
+
# long-lived secret to leak or rotate; scoped by PyPI to exactly this repository and
|
|
21
|
+
# workflow file. Requires a one-time setup on pypi.org (see `docs/RELEASING.md`)
|
|
22
|
+
# before the first real run — until that's done, this job's publish step fails with a
|
|
23
|
+
# clear PyPI-side error, not a silent no-op.
|
|
24
|
+
|
|
25
|
+
name: Release
|
|
26
|
+
|
|
27
|
+
on:
|
|
28
|
+
push:
|
|
29
|
+
tags:
|
|
30
|
+
- "v*"
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
release:
|
|
34
|
+
name: Build, verify and publish
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: pypi
|
|
37
|
+
# Job-level `permissions:` REPLACES the workflow-level block for this job, it does
|
|
38
|
+
# not merge with it — a real bug found running publish-testpypi.yml's identical
|
|
39
|
+
# shape for the first time (2026-08-26): `contents: read` lived only at the
|
|
40
|
+
# workflow level, so the job's own `permissions: {id-token: write}` (needed for
|
|
41
|
+
# Trusted Publishing's OIDC exchange) silently dropped it, and actions/checkout
|
|
42
|
+
# got a token with no read access to the repo at all ("repository ... not found").
|
|
43
|
+
# Both permissions this job actually needs are listed here, explicitly, together.
|
|
44
|
+
permissions:
|
|
45
|
+
contents: read
|
|
46
|
+
id-token: write
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- name: Derive version from the tag
|
|
52
|
+
id: version
|
|
53
|
+
run: |
|
|
54
|
+
tag="${GITHUB_REF_NAME}"
|
|
55
|
+
version="${tag#v}"
|
|
56
|
+
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc|\.dev)[0-9]+)?$ ]]; then
|
|
57
|
+
echo "::error::tag '$tag' does not look like vMAJOR.MINOR.PATCH, optionally suffixed a[N]/b[N]/rc[N]/.dev[N] (PEP 440)"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
61
|
+
|
|
62
|
+
- name: Rewrite version to match the tag
|
|
63
|
+
# Not committed back — this mutates the ephemeral checkout only, so the
|
|
64
|
+
# build artifact carries the tag's version without the repository ever
|
|
65
|
+
# holding a second, potentially-stale copy of it outside a release.
|
|
66
|
+
# Plain `sed`, deliberately — a shell one-liner is easier to verify at a
|
|
67
|
+
# glance than a heredoc'd Python string embedded inside a shell step.
|
|
68
|
+
run: |
|
|
69
|
+
version="${{ steps.version.outputs.version }}"
|
|
70
|
+
sed -i "s/^version = \".*\"/version = \"${version}\"/" pyproject.toml
|
|
71
|
+
sed -i "s/^__version__ = \".*\"/__version__ = \"${version}\"/" src/salak/__init__.py
|
|
72
|
+
grep -q "version = \"${version}\"" pyproject.toml
|
|
73
|
+
grep -q "__version__ = \"${version}\"" src/salak/__init__.py
|
|
74
|
+
|
|
75
|
+
- name: Install uv
|
|
76
|
+
uses: astral-sh/setup-uv@v5
|
|
77
|
+
with:
|
|
78
|
+
enable-cache: true
|
|
79
|
+
|
|
80
|
+
- name: Sync dependencies
|
|
81
|
+
run: uv sync --python "3.11"
|
|
82
|
+
|
|
83
|
+
- name: Verify the retagged tree (lint, type-check, package-metadata + version tests)
|
|
84
|
+
# Full pytest is CI's job (already green on the commit this tag points at);
|
|
85
|
+
# here it's the version-sync and packaging tests specifically that matter,
|
|
86
|
+
# since those are what the rewrite step above could break.
|
|
87
|
+
run: |
|
|
88
|
+
uv run ruff check .
|
|
89
|
+
uv run mypy src tests
|
|
90
|
+
uv run pytest tests/test_cli_version.py tests/test_packaging_metadata.py tests/test_wheel_installation.py -q
|
|
91
|
+
|
|
92
|
+
- name: Build
|
|
93
|
+
run: uv build
|
|
94
|
+
|
|
95
|
+
- name: Publish to PyPI
|
|
96
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
salak-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
# Tooling caches
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
|
|
15
|
+
# Editors / OS
|
|
16
|
+
.idea/
|
|
17
|
+
.vscode/
|
|
18
|
+
.DS_Store
|
|
19
|
+
|
|
20
|
+
# Spike 0 raw evidence — local only. Finding G3.
|
|
21
|
+
# The 40 log files enumerate the internal structure of jeruk-ai and mangga-pa: route
|
|
22
|
+
# names, admin pages, auth flows, module layout, and the absolute paths of the machine
|
|
23
|
+
# they were measured on. No secret *values* (verified), but this package is destined
|
|
24
|
+
# for PyPI with a public GitHub homepage, and publishing another project's file
|
|
25
|
+
# inventory is cheap to prevent and impossible to undo. The findings that cite them
|
|
26
|
+
# stay tracked; the logs stay on disk, uncommitted, and `README.md` in that directory
|
|
27
|
+
# says so.
|
|
28
|
+
project-memory/spike-0/logs/
|
|
29
|
+
|
|
30
|
+
# NOTE: project-memory/repo-graph.json is NOT ignored.
|
|
31
|
+
# It is the artifact this tool exists to produce, and SDD §9 Q2 decided it is
|
|
32
|
+
# committed to git for v0.1 (1.08 MB measured, five times under the threshold).
|
|
33
|
+
|
|
34
|
+
# Language servers for the Phase 2 tests. Pinned in package.json, never committed:
|
|
35
|
+
# they are ~100 MB of platform-specific JavaScript, and `npm ci` reproduces them exactly.
|
|
36
|
+
node_modules/
|