embeddings-cli 0.9.0__tar.gz → 0.9.1__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.
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.github/workflows/tests.yml +15 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/CHANGELOG.md +10 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/PKG-INFO +1 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/export.py +1 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/gaps.py +21 -11
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/map.py +7 -3
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/regions.py +7 -3
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/pyproject.toml +1 -1
- embeddings_cli-0.9.1/scripts/sonar_open_issues.py +147 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_backend_client.py +6 -3
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_cli_contract.py +2 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_corpus.py +2 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_directions.py +8 -4
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_export.py +16 -8
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_gaps.py +2 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_neighbors.py +2 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_numeric.py +2 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_regions.py +2 -1
- embeddings_cli-0.9.1/tests/test_sonar_gate.py +152 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_stability.py +2 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/uv.lock +1 -1
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/agent-config/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/agent-config/scripts/show.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/ask-colleague/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/ask-colleague/prompts/review.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/ask-colleague/prompts/write.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/challenge/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/cicd/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/cicd/scripts/workflow.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/communicate/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/deviate/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/recall/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/recall/scripts/recall.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/remember/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/remember/scripts/remember.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/run-tests/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/scope/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/sonarclaude/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/spec-to-plan/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/summarize-delivery/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/think/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/think/scripts/think.sh +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/version-bump/SKILL.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills/version-bump/scripts/bump.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.claude/skills.local.yaml.example +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.devague/current +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.devague/current_plan +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.devague/frames/embeddings-survey-surface.json +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.devague/plans/embeddings-survey-surface.json +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.flake8 +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.github/workflows/publish.yml +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.gitignore +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/.markdownlint-cli2.yaml +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/AGENTS.colleague.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/CLAUDE.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/LICENSE +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/README.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/culture.yaml +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/design-notes/semantic-measurement.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/evidence/2026-07-20-live-end-to-end.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/evidence/build_corpus.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/evidence/corpus.jsonl +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/evidence/map.html +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/evidence/pairs-legal-to-technical.txt +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/evidence/probes-legal.txt +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/plans/2026-07-20-embeddings-survey-surface.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/skill-sources.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/specs/2026-07-20-embeddings-survey-surface.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/2026-07-20-eidetic-embedder.md +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/atlas-instruct/atlas.json +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/atlas-instruct/records.jsonl +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/atlas-long/atlas.json +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/atlas-long/records.jsonl +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/atlas-plain/atlas.json +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/atlas-plain/records.jsonl +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/build_probe_corpus.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/measure_pairs.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/pairs-contra-high.txt +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/pairs-contra-low.txt +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/pairs-negation-contra.txt +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/pairs-para-high.txt +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/pairs-value-contra.txt +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/probe-corpus-instruct.jsonl +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/probe-corpus-long.jsonl +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/probe-corpus.jsonl +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/probes-heldout.txt +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/docs/studies/probes.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/__init__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/__main__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/_atomic.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/_numeric.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/atlas/__init__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/atlas/io.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/atlas/schema.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/backends/__init__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/backends/cache.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/backends/client.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/__init__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/__init__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/_atlas_arg.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/cli.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/compare.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/directions.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/doctor.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/explain.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/learn.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/neighbors.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/overview.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/survey.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_commands/whoami.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_errors.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_output.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/cli/_provenance.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/corpus.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/explain/__init__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/explain/catalog.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/measure/__init__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/embeddings/measure/stability.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/sonar-project.properties +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/__init__.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/_fakes.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/_viewer_contract.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_atlas_arg.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_atlas_io.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_atlas_schema.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_atomic.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_backend_cache.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_cli.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_cli_introspection.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_compare.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_fakes.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_map.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_provenance.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_roadmap_lockstep.py +0 -0
- {embeddings_cli-0.9.0 → embeddings_cli-0.9.1}/tests/test_survey.py +0 -0
|
@@ -37,6 +37,21 @@ jobs:
|
|
|
37
37
|
env:
|
|
38
38
|
SONAR_HOST_URL: https://sonarcloud.io
|
|
39
39
|
|
|
40
|
+
# Stricter than the quality gate: any issue left Open or Confirmed fails the run.
|
|
41
|
+
# An issue marked Accepted on SonarCloud does not. The scan step above waits for
|
|
42
|
+
# the analysis (sonar.qualitygate.wait), so the issue list is complete here.
|
|
43
|
+
- name: No open SonarCloud issues
|
|
44
|
+
if: env.SONAR_TOKEN != ''
|
|
45
|
+
env:
|
|
46
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
47
|
+
BRANCH_NAME: ${{ github.ref_name }}
|
|
48
|
+
run: |
|
|
49
|
+
if [ -n "$PR_NUMBER" ]; then
|
|
50
|
+
python3 scripts/sonar_open_issues.py --pull-request "$PR_NUMBER"
|
|
51
|
+
else
|
|
52
|
+
python3 scripts/sonar_open_issues.py --branch "$BRANCH_NAME"
|
|
53
|
+
fi
|
|
54
|
+
|
|
40
55
|
lint:
|
|
41
56
|
runs-on: ubuntu-latest
|
|
42
57
|
permissions:
|
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.9.1] - 2026-11-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- CI fails when SonarCloud holds an issue left Open or Confirmed for the pull request or, on main, for the branch (`scripts/sonar_open_issues.py`, run after the scan). An issue marked Accepted does not fail it.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- The 28 SonarCloud issues open on main: composite assertions split in the tests, runs of `append` replaced by `extend` in `gaps`, `map` and `regions`, and the redacted parameters of `export` built as a dict.
|
|
17
|
+
|
|
8
18
|
## [0.9.0] - 2026-11-13
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: embeddings-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Maps the geometry of meaning produced by embedding models — survey a corpus into a semantic landscape, then inspect its regions, neighborhoods, directions, and gaps, and compare models. Backs a Surveyor agent that runs measured experiments against the space and records an evidence-backed Atlas.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agentculture/embeddings-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/agentculture/embeddings-cli/issues
|
|
@@ -359,7 +359,7 @@ def _provenance(atlas: Atlas, endpoint_name: str | None) -> dict:
|
|
|
359
359
|
}
|
|
360
360
|
if endpoint_name is not None:
|
|
361
361
|
provenance["endpoint"] = endpoint_name
|
|
362
|
-
params = _redact(
|
|
362
|
+
params = {key: _redact(item, needles) for key, item in manifest.params.items()}
|
|
363
363
|
corpus = params.get("corpus")
|
|
364
364
|
if isinstance(corpus, str) and corpus:
|
|
365
365
|
# The file's own name only (either separator), never the folders above it.
|
|
@@ -1003,18 +1003,24 @@ def _render(payload: dict) -> str:
|
|
|
1003
1003
|
"",
|
|
1004
1004
|
]
|
|
1005
1005
|
for finding in payload["corpus_coverage_findings"]:
|
|
1006
|
-
lines.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1006
|
+
lines.extend(
|
|
1007
|
+
[
|
|
1008
|
+
f"- [{finding['type']}] {finding['shows']}",
|
|
1009
|
+
f" does not show: {finding['does_not_show']}",
|
|
1010
|
+
f" asserted: {finding['asserted']} ({finding['stability_verdict']})",
|
|
1011
|
+
"",
|
|
1012
|
+
]
|
|
1013
|
+
)
|
|
1010
1014
|
if not payload["corpus_coverage_findings"]:
|
|
1011
1015
|
lines += ["(no area of this atlas met the sparsity or void thresholds above)", ""]
|
|
1012
1016
|
|
|
1013
|
-
lines.
|
|
1014
|
-
|
|
1017
|
+
lines.extend(
|
|
1018
|
+
[
|
|
1019
|
+
f"## model representation — {payload['counts']['model_representation']} finding(s)",
|
|
1020
|
+
" what probing actually showed. Only ever produced by a live probe step.",
|
|
1021
|
+
"",
|
|
1022
|
+
]
|
|
1015
1023
|
)
|
|
1016
|
-
lines.append(" what probing actually showed. Only ever produced by a live probe step.")
|
|
1017
|
-
lines.append("")
|
|
1018
1024
|
if not step["requested"]:
|
|
1019
1025
|
lines += [
|
|
1020
1026
|
"no probe step ran (probing is off by default), so no claim about the model is "
|
|
@@ -1031,9 +1037,13 @@ def _render(payload: dict) -> str:
|
|
|
1031
1037
|
]
|
|
1032
1038
|
else:
|
|
1033
1039
|
for finding in payload["model_representation_findings"]:
|
|
1034
|
-
lines.
|
|
1035
|
-
|
|
1036
|
-
|
|
1040
|
+
lines.extend(
|
|
1041
|
+
[
|
|
1042
|
+
f"- [{finding['verdict']}] {finding['shows']}",
|
|
1043
|
+
f" does not show: {finding['does_not_show']}",
|
|
1044
|
+
"",
|
|
1045
|
+
]
|
|
1046
|
+
)
|
|
1037
1047
|
if not payload["model_representation_findings"]:
|
|
1038
1048
|
lines += ["(the probe step ran but produced no finding; see inconclusive below)", ""]
|
|
1039
1049
|
|
|
@@ -614,9 +614,13 @@ def _render_text(payload: dict) -> str:
|
|
|
614
614
|
lines.append("re-run with --apply to write the artifact. Nothing was written.")
|
|
615
615
|
lines.append("")
|
|
616
616
|
|
|
617
|
-
lines.
|
|
618
|
-
|
|
619
|
-
|
|
617
|
+
lines.extend(
|
|
618
|
+
[
|
|
619
|
+
payload["caveat"],
|
|
620
|
+
"",
|
|
621
|
+
"uncertain (belongs in the atlas manifest's uncertain[]):",
|
|
622
|
+
]
|
|
623
|
+
)
|
|
620
624
|
lines += [f"- {entry}" for entry in payload["uncertain"]]
|
|
621
625
|
lines.append("")
|
|
622
626
|
lines.append(reproduce_line(payload[REPRODUCED_BY_KEY]))
|
|
@@ -489,9 +489,13 @@ def _render(payload: dict) -> str:
|
|
|
489
489
|
]
|
|
490
490
|
for region in payload["regions"]:
|
|
491
491
|
names = ", ".join(item["id"] for item in region["representatives"])
|
|
492
|
-
lines.
|
|
493
|
-
|
|
494
|
-
|
|
492
|
+
lines.extend(
|
|
493
|
+
[
|
|
494
|
+
f"region {region['id']} — {region['size']} items",
|
|
495
|
+
f" representatives: {names}",
|
|
496
|
+
f" stability: {region['stability']['verdict']}",
|
|
497
|
+
]
|
|
498
|
+
)
|
|
495
499
|
if region["finding"]:
|
|
496
500
|
lines.append(f" finding: {region['finding']}")
|
|
497
501
|
lines.append("")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "embeddings-cli"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Maps the geometry of meaning produced by embedding models — survey a corpus into a semantic landscape, then inspect its regions, neighborhoods, directions, and gaps, and compare models. Backs a Surveyor agent that runs measured experiments against the space and records an evidence-backed Atlas."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fail when SonarCloud holds open issues for this pull request or branch.
|
|
3
|
+
|
|
4
|
+
python3 scripts/sonar_open_issues.py --pull-request 12
|
|
5
|
+
python3 scripts/sonar_open_issues.py --branch main
|
|
6
|
+
|
|
7
|
+
The quality gate only looks at new code against thresholds, so a pull request can pass it
|
|
8
|
+
while carrying open issues. This check is stricter: any issue whose status is Open or
|
|
9
|
+
Confirmed fails the run. An issue marked Accepted (or False positive, or Fixed) does not:
|
|
10
|
+
accepting an issue on SonarCloud is the way to keep it on purpose.
|
|
11
|
+
|
|
12
|
+
It reads the project key from ``sonar-project.properties`` and asks SonarCloud's public
|
|
13
|
+
web API for the issues of one analysis scope: a pull request (the issues its analysis
|
|
14
|
+
found in the changed code) or a branch (every issue on it). CI runs it straight after
|
|
15
|
+
the scan step, which waits for the analysis to finish (``sonar.qualitygate.wait``).
|
|
16
|
+
|
|
17
|
+
The token, if ``SONAR_TOKEN`` is set, is sent only to SonarCloud as a bearer header and
|
|
18
|
+
is never printed. Standard library only.
|
|
19
|
+
|
|
20
|
+
Exit codes: 0 no open issue, 1 open issues (each one is listed), 2 the check could not
|
|
21
|
+
be made (no project key, the request failed, or the answer was not understood).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
import urllib.error
|
|
31
|
+
import urllib.parse
|
|
32
|
+
import urllib.request
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
HOST = "https://sonarcloud.io"
|
|
36
|
+
OPEN_STATUSES = frozenset({"OPEN", "CONFIRMED"})
|
|
37
|
+
PAGE_SIZE = 500
|
|
38
|
+
#: SonarCloud serves at most 10,000 results per search; stop there and say so.
|
|
39
|
+
MAX_RESULTS = 10_000
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CheckError(Exception):
|
|
43
|
+
"""The check could not be made (distinct from "open issues were found")."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def project_key(properties: Path) -> str:
|
|
47
|
+
"""``sonar.projectKey`` from a ``sonar-project.properties`` file."""
|
|
48
|
+
try:
|
|
49
|
+
lines = properties.read_text(encoding="utf-8").splitlines()
|
|
50
|
+
except OSError as err:
|
|
51
|
+
raise CheckError(f"cannot read {properties}: {err}") from err
|
|
52
|
+
for line in lines:
|
|
53
|
+
name, sep, value = line.partition("=")
|
|
54
|
+
if sep and name.strip() == "sonar.projectKey" and value.strip():
|
|
55
|
+
return value.strip()
|
|
56
|
+
raise CheckError(f"{properties} has no sonar.projectKey")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fetch_json(url: str, token: str | None) -> dict:
|
|
60
|
+
"""GET one page of the web API. Only ever called with a SonarCloud https URL."""
|
|
61
|
+
if not url.startswith(HOST + "/"):
|
|
62
|
+
raise CheckError("refusing to request an address outside SonarCloud")
|
|
63
|
+
request = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
64
|
+
if token:
|
|
65
|
+
request.add_header("Authorization", f"Bearer {token}")
|
|
66
|
+
try:
|
|
67
|
+
with urllib.request.urlopen(request, timeout=60) as response: # nosec B310
|
|
68
|
+
return json.loads(response.read().decode("utf-8"))
|
|
69
|
+
except urllib.error.HTTPError as err:
|
|
70
|
+
raise CheckError(f"SonarCloud answered HTTP {err.code} for the issue search") from err
|
|
71
|
+
except (urllib.error.URLError, TimeoutError, ValueError) as err:
|
|
72
|
+
raise CheckError(f"could not read the issue search from SonarCloud: {err}") from err
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_open(issue: dict) -> bool:
|
|
76
|
+
"""Open or Confirmed. Accepted, False positive and Fixed are not open."""
|
|
77
|
+
status = issue.get("issueStatus") or issue.get("status") or ""
|
|
78
|
+
return str(status).upper() in OPEN_STATUSES
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def open_issues(key: str, scope: dict[str, str], token: str | None, fetch=fetch_json) -> list[dict]:
|
|
82
|
+
"""Every open issue of the project in ``scope`` (``pullRequest`` or ``branch``)."""
|
|
83
|
+
found: list[dict] = []
|
|
84
|
+
page = 1
|
|
85
|
+
while True:
|
|
86
|
+
query = {"componentKeys": key, "resolved": "false", "ps": str(PAGE_SIZE), "p": str(page)}
|
|
87
|
+
query.update(scope)
|
|
88
|
+
data = fetch(f"{HOST}/api/issues/search?{urllib.parse.urlencode(query)}", token)
|
|
89
|
+
if not isinstance(data, dict) or "errors" in data or "issues" not in data:
|
|
90
|
+
detail = data.get("errors") if isinstance(data, dict) else data
|
|
91
|
+
raise CheckError(f"SonarCloud did not return an issue list: {detail}")
|
|
92
|
+
issues = data["issues"]
|
|
93
|
+
found.extend(issue for issue in issues if is_open(issue))
|
|
94
|
+
total = int(data.get("total", data.get("paging", {}).get("total", 0)))
|
|
95
|
+
if not issues or page * PAGE_SIZE >= min(total, MAX_RESULTS):
|
|
96
|
+
if total > MAX_RESULTS:
|
|
97
|
+
raise CheckError(f"more than {MAX_RESULTS} issues; the list is incomplete")
|
|
98
|
+
return found
|
|
99
|
+
page += 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def describe(issue: dict) -> str:
|
|
103
|
+
component = str(issue.get("component", ""))
|
|
104
|
+
path = component.split(":", 1)[1] if ":" in component else component
|
|
105
|
+
line = issue.get("line")
|
|
106
|
+
where = f"{path}:{line}" if line else path
|
|
107
|
+
return (
|
|
108
|
+
f"{where} {issue.get('rule', '?')} "
|
|
109
|
+
f"{str(issue.get('severity', '')).lower()} {issue.get('message', '')}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv: list[str] | None = None) -> int:
|
|
114
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
115
|
+
scope = parser.add_mutually_exclusive_group(required=True)
|
|
116
|
+
scope.add_argument("--pull-request", metavar="<number>", help="check this pull request")
|
|
117
|
+
scope.add_argument("--branch", metavar="<name>", help="check this branch")
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--properties",
|
|
120
|
+
type=Path,
|
|
121
|
+
default=Path(__file__).resolve().parents[1] / "sonar-project.properties",
|
|
122
|
+
help="the sonar-project.properties to read the project key from",
|
|
123
|
+
)
|
|
124
|
+
args = parser.parse_args(argv)
|
|
125
|
+
where = {"pullRequest": args.pull_request} if args.pull_request else {"branch": args.branch}
|
|
126
|
+
label = f"pull request {args.pull_request}" if args.pull_request else f"branch {args.branch}"
|
|
127
|
+
try:
|
|
128
|
+
key = project_key(args.properties)
|
|
129
|
+
issues = open_issues(key, where, os.environ.get("SONAR_TOKEN") or None)
|
|
130
|
+
except CheckError as err:
|
|
131
|
+
print(f"sonar-open-issues: could not check {label}: {err}", file=sys.stderr)
|
|
132
|
+
return 2
|
|
133
|
+
if not issues:
|
|
134
|
+
print(f"sonar-open-issues: no open issue on {label} of {key}")
|
|
135
|
+
return 0
|
|
136
|
+
print(f"sonar-open-issues: {len(issues)} open issue(s) on {label} of {key}:", file=sys.stderr)
|
|
137
|
+
for issue in issues:
|
|
138
|
+
print(" " + describe(issue), file=sys.stderr)
|
|
139
|
+
print(
|
|
140
|
+
"fix each one, or mark it Accepted on SonarCloud if it is kept on purpose",
|
|
141
|
+
file=sys.stderr,
|
|
142
|
+
)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
raise SystemExit(main())
|
|
@@ -247,7 +247,8 @@ def test_short_data_list_raises_cli_error_code_2() -> None:
|
|
|
247
247
|
client.embed(["hello", "world"])
|
|
248
248
|
|
|
249
249
|
assert exc_info.value.code == EXIT_ENV_ERROR
|
|
250
|
-
assert "1" in exc_info.value.message
|
|
250
|
+
assert "1" in exc_info.value.message
|
|
251
|
+
assert "2" in exc_info.value.message
|
|
251
252
|
assert "one embedding per input" in exc_info.value.remediation
|
|
252
253
|
|
|
253
254
|
|
|
@@ -461,10 +462,12 @@ def test_no_socket_opened_when_transport_is_injected(monkeypatch: pytest.MonkeyP
|
|
|
461
462
|
|
|
462
463
|
def test_endpoint_help_text_states_off_machine_implication() -> None:
|
|
463
464
|
lowered = ENDPOINT_HELP.lower()
|
|
464
|
-
assert "off" in lowered
|
|
465
|
+
assert "off" in lowered
|
|
466
|
+
assert "machine" in lowered
|
|
465
467
|
assert DEFAULT_BASE_URL in ENDPOINT_HELP
|
|
466
468
|
assert ENV_BASE_URL in ENDPOINT_HELP
|
|
467
|
-
assert isinstance(ENDPOINT_FLAG, str)
|
|
469
|
+
assert isinstance(ENDPOINT_FLAG, str)
|
|
470
|
+
assert ENDPOINT_FLAG.startswith("--")
|
|
468
471
|
|
|
469
472
|
|
|
470
473
|
# --------------------------------------------------------------------------
|
|
@@ -213,7 +213,8 @@ def test_every_subcommand_invalid_flag_is_a_structured_error_json_mode(
|
|
|
213
213
|
), f"'embeddings {' '.join(path)} --json {_BOGUS_FLAG}' leaked a result onto stdout"
|
|
214
214
|
payload = json.loads(captured.err)
|
|
215
215
|
assert payload["code"] == 1
|
|
216
|
-
assert isinstance(payload["message"], str)
|
|
216
|
+
assert isinstance(payload["message"], str)
|
|
217
|
+
assert payload["message"]
|
|
217
218
|
assert "remediation" in payload
|
|
218
219
|
|
|
219
220
|
|
|
@@ -237,7 +237,8 @@ def test_duplicate_ids_are_refused_naming_both_lines(tmp_path) -> None:
|
|
|
237
237
|
load_corpus(path)
|
|
238
238
|
assert exc.value.code == 1
|
|
239
239
|
# Both the offending line and the first occurrence, so the fix is obvious.
|
|
240
|
-
assert "3" in exc.value.message
|
|
240
|
+
assert "3" in exc.value.message
|
|
241
|
+
assert "1" in exc.value.message
|
|
241
242
|
assert "shared" in exc.value.message
|
|
242
243
|
assert "unique" in exc.value.remediation
|
|
243
244
|
|
|
@@ -143,10 +143,12 @@ def test_directions_overview_json_returns_the_sections_payload(
|
|
|
143
143
|
) -> None:
|
|
144
144
|
payload = _run_json(capsys, ["directions", "overview", "--json"])
|
|
145
145
|
assert payload["subject"] == "embeddings-cli directions"
|
|
146
|
-
assert isinstance(payload["sections"], list)
|
|
146
|
+
assert isinstance(payload["sections"], list)
|
|
147
|
+
assert payload["sections"]
|
|
147
148
|
for section in payload["sections"]:
|
|
148
149
|
assert section["title"]
|
|
149
|
-
assert isinstance(section["items"], list)
|
|
150
|
+
assert isinstance(section["items"], list)
|
|
151
|
+
assert section["items"]
|
|
150
152
|
|
|
151
153
|
|
|
152
154
|
def test_bare_directions_json_matches_overview_json(capsys: pytest.CaptureFixture[str]) -> None:
|
|
@@ -364,7 +366,8 @@ def test_holdout_pairs_are_disjoint_from_the_pairs_the_direction_was_derived_fro
|
|
|
364
366
|
|
|
365
367
|
train = {(entry["a"], entry["b"]) for entry in candidate["train"]["pairs_used"]}
|
|
366
368
|
holdout = {(entry["a"], entry["b"]) for entry in candidate["holdout"]["pairs_used"]}
|
|
367
|
-
assert train
|
|
369
|
+
assert train
|
|
370
|
+
assert holdout
|
|
368
371
|
assert not (train & holdout)
|
|
369
372
|
assert len(train) + len(holdout) == PAIR_COUNT
|
|
370
373
|
assert payload["pairs"]["train"] == len(train)
|
|
@@ -432,7 +435,8 @@ def test_a_planted_direction_is_stable_across_seed_reruns(
|
|
|
432
435
|
payload = _run_json(capsys, _discover(planted_atlas, pairs, "--top", "1", "--runs", "4"))
|
|
433
436
|
stability = payload["candidates"][0]["stability"]
|
|
434
437
|
assert stability["verified"] is True
|
|
435
|
-
assert stability["score"] is not None
|
|
438
|
+
assert stability["score"] is not None
|
|
439
|
+
assert stability["score"] > 0.5
|
|
436
440
|
|
|
437
441
|
|
|
438
442
|
# --------------------------------------------------------------------------
|
|
@@ -383,7 +383,8 @@ def test_a_nan_in_metadata_is_refused_not_written_as_broken_json(tmp_path, capsy
|
|
|
383
383
|
rc, err = run(capsys, "--atlas", str(target), "--out", str(out), "--apply")
|
|
384
384
|
assert rc == 1
|
|
385
385
|
assert "JSON" in err["message"]
|
|
386
|
-
assert not out.exists()
|
|
386
|
+
assert not out.exists()
|
|
387
|
+
assert not (tmp_path / "v.f32").exists()
|
|
387
388
|
|
|
388
389
|
|
|
389
390
|
def test_a_seed_beyond_the_viewers_integer_range_is_refused(tmp_path, capsys) -> None:
|
|
@@ -408,8 +409,10 @@ def test_a_value_that_overflows_float32_is_refused(tmp_path, capsys) -> None:
|
|
|
408
409
|
out = tmp_path / "v.json"
|
|
409
410
|
rc, err = run(capsys, "--atlas", str(target), "--out", str(out), "--apply")
|
|
410
411
|
assert rc == 2
|
|
411
|
-
assert "doc-2" in err["message"]
|
|
412
|
-
assert
|
|
412
|
+
assert "doc-2" in err["message"]
|
|
413
|
+
assert "float32" in err["message"]
|
|
414
|
+
assert not out.exists()
|
|
415
|
+
assert not (tmp_path / "v.f32").exists()
|
|
413
416
|
|
|
414
417
|
|
|
415
418
|
def test_atlas_without_vectors_is_refused_with_a_survey_hint(tmp_path: Path, capsys) -> None:
|
|
@@ -799,7 +802,8 @@ def test_no_group_is_written_without_group_by(atlas: Path, tmp_path: Path, capsy
|
|
|
799
802
|
out = tmp_path / "v.json"
|
|
800
803
|
rc, payload = run(capsys, "--atlas", str(atlas), "--out", str(out), "--apply")
|
|
801
804
|
assert rc == 0
|
|
802
|
-
assert payload["group_by"] is None
|
|
805
|
+
assert payload["group_by"] is None
|
|
806
|
+
assert payload["groups"] == 0
|
|
803
807
|
data, _ = load_pair(out)
|
|
804
808
|
assert all("group" not in point for point in data["points"])
|
|
805
809
|
|
|
@@ -813,7 +817,8 @@ def test_group_by_writes_each_points_group_from_metadata(
|
|
|
813
817
|
)
|
|
814
818
|
assert rc == 0
|
|
815
819
|
assert payload["group_by"] == "topic"
|
|
816
|
-
assert payload["groups"] == 3
|
|
820
|
+
assert payload["groups"] == 3
|
|
821
|
+
assert payload["grouped_points"] == 12
|
|
817
822
|
assert check_pair(out) == []
|
|
818
823
|
data, _ = load_pair(out)
|
|
819
824
|
assert [point["group"] for point in data["points"]] == [f"topic-{i % 3}" for i in range(12)]
|
|
@@ -837,7 +842,8 @@ def test_group_by_leaves_records_without_a_usable_value_ungrouped(tmp_path: Path
|
|
|
837
842
|
capsys, "--atlas", str(target), "--out", str(out), "--apply", "--group-by", "kind"
|
|
838
843
|
)
|
|
839
844
|
assert rc == 0, payload
|
|
840
|
-
assert payload["grouped_points"] == 2
|
|
845
|
+
assert payload["grouped_points"] == 2
|
|
846
|
+
assert payload["groups"] == 2
|
|
841
847
|
data, _ = load_pair(out)
|
|
842
848
|
assert [point.get("group") for point in data["points"]] == ["alpha", "7"] + [None] * 4
|
|
843
849
|
assert check_pair(out) == []
|
|
@@ -850,7 +856,8 @@ def test_group_by_a_key_no_record_carries_is_refused(atlas: Path, tmp_path: Path
|
|
|
850
856
|
)
|
|
851
857
|
assert rc == 1
|
|
852
858
|
assert "topik" in payload["message"]
|
|
853
|
-
assert not out.exists()
|
|
859
|
+
assert not out.exists()
|
|
860
|
+
assert not (tmp_path / "v.f32").exists()
|
|
854
861
|
|
|
855
862
|
|
|
856
863
|
def test_group_by_with_labels_only_is_refused(atlas: Path, tmp_path: Path, capsys) -> None:
|
|
@@ -875,6 +882,7 @@ def test_the_corpus_path_is_reduced_to_its_file_name(tmp_path: Path, capsys) ->
|
|
|
875
882
|
rc, _ = run(capsys, "--atlas", str(target), "--out", str(out), "--apply")
|
|
876
883
|
assert rc == 0
|
|
877
884
|
raw = out.read_bytes()
|
|
878
|
-
assert b"someone" not in raw
|
|
885
|
+
assert b"someone" not in raw
|
|
886
|
+
assert b"private-project" not in raw
|
|
879
887
|
data, _ = load_pair(out)
|
|
880
888
|
assert data["provenance"]["params"]["corpus"] == "corpus.jsonl"
|
|
@@ -580,7 +580,8 @@ def test_probe_flag_without_a_source_is_a_user_error(
|
|
|
580
580
|
|
|
581
581
|
assert code == 1
|
|
582
582
|
err = capsys.readouterr().err
|
|
583
|
-
assert "--probes" in err
|
|
583
|
+
assert "--probes" in err
|
|
584
|
+
assert "--holdout" in err
|
|
584
585
|
|
|
585
586
|
|
|
586
587
|
def test_missing_probes_file_is_a_user_error(
|
|
@@ -473,7 +473,8 @@ def test_boundary_reports_the_largest_gap_between_b_c_and_c_d(
|
|
|
473
473
|
assert boundary["gaps"] == pytest.approx(expected_gaps, abs=1e-6)
|
|
474
474
|
assert boundary["largest_gap_index"] == expected_gaps.index(max(expected_gaps))
|
|
475
475
|
assert boundary["largest_gap"] == pytest.approx(max(expected_gaps), abs=1e-6)
|
|
476
|
-
assert isinstance(boundary["note"], str)
|
|
476
|
+
assert isinstance(boundary["note"], str)
|
|
477
|
+
assert boundary["note"]
|
|
477
478
|
|
|
478
479
|
|
|
479
480
|
def test_boundary_with_fewer_than_two_neighbors_reports_no_gap(
|
|
@@ -55,7 +55,8 @@ def test_distances_are_correct_and_bounded() -> None:
|
|
|
55
55
|
assert distances[0] == pytest.approx(0.0)
|
|
56
56
|
assert distances[1] == pytest.approx(1.0)
|
|
57
57
|
assert distances[2] == pytest.approx(2.0)
|
|
58
|
-
assert np.all(distances >= 0.0)
|
|
58
|
+
assert np.all(distances >= 0.0)
|
|
59
|
+
assert np.all(distances <= 2.0)
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
def test_an_identical_vector_never_yields_a_negative_distance() -> None:
|
|
@@ -332,7 +332,8 @@ def test_single_run_regions_are_unverified_findings_about_the_parameters(
|
|
|
332
332
|
assert region["stability"]["verdict"] == VERDICT_UNVERIFIED
|
|
333
333
|
assert region["stability"]["score"] is None
|
|
334
334
|
finding = region["finding"]
|
|
335
|
-
assert isinstance(finding, str)
|
|
335
|
+
assert isinstance(finding, str)
|
|
336
|
+
assert finding
|
|
336
337
|
assert "parameters" in finding
|
|
337
338
|
assert "k=3" in finding
|
|
338
339
|
assert "not" in finding
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""The CI check that fails on open SonarCloud issues (scripts/sonar_open_issues.py).
|
|
2
|
+
|
|
3
|
+
No request leaves the machine: the fetch function is replaced by a fake.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
14
|
+
SCRIPT = REPO_ROOT / "scripts" / "sonar_open_issues.py"
|
|
15
|
+
|
|
16
|
+
spec = importlib.util.spec_from_file_location("sonar_open_issues", SCRIPT)
|
|
17
|
+
gate = importlib.util.module_from_spec(spec)
|
|
18
|
+
spec.loader.exec_module(gate)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def issue(status: str, n: int = 1, legacy: bool = False) -> dict:
|
|
22
|
+
record = {
|
|
23
|
+
"rule": "python:S9073",
|
|
24
|
+
"severity": "MAJOR",
|
|
25
|
+
"component": "agentculture_demo:tests/test_x.py",
|
|
26
|
+
"line": n,
|
|
27
|
+
"message": f"message {n}",
|
|
28
|
+
}
|
|
29
|
+
record["status" if legacy else "issueStatus"] = status
|
|
30
|
+
return record
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def fake(pages: list[dict]):
|
|
34
|
+
calls: list[tuple[str, str | None]] = []
|
|
35
|
+
|
|
36
|
+
def fetch(url: str, token: str | None) -> dict:
|
|
37
|
+
calls.append((url, token))
|
|
38
|
+
return pages[min(len(calls), len(pages)) - 1]
|
|
39
|
+
|
|
40
|
+
return fetch, calls
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_project_key_is_read_from_this_repos_properties() -> None:
|
|
44
|
+
assert gate.project_key(REPO_ROOT / "sonar-project.properties") == "agentculture_embeddings-cli"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_project_key_missing_is_a_check_error(tmp_path: Path) -> None:
|
|
48
|
+
(tmp_path / "p").write_text("sonar.organization=x\n", encoding="utf-8")
|
|
49
|
+
with pytest.raises(gate.CheckError):
|
|
50
|
+
gate.project_key(tmp_path / "p")
|
|
51
|
+
with pytest.raises(gate.CheckError):
|
|
52
|
+
gate.project_key(tmp_path / "absent")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize(
|
|
56
|
+
"status,is_open",
|
|
57
|
+
[
|
|
58
|
+
("OPEN", True),
|
|
59
|
+
("CONFIRMED", True),
|
|
60
|
+
("ACCEPTED", False),
|
|
61
|
+
("FALSE_POSITIVE", False),
|
|
62
|
+
("FIXED", False),
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
def test_only_open_and_confirmed_count(status: str, is_open: bool) -> None:
|
|
66
|
+
assert gate.is_open(issue(status)) is is_open
|
|
67
|
+
assert gate.is_open(issue(status, legacy=True)) is is_open
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_scope_and_project_are_sent_and_accepted_issues_are_ignored() -> None:
|
|
71
|
+
fetch, calls = fake([{"total": 3, "issues": [issue("OPEN", 1), issue("ACCEPTED", 2)]}])
|
|
72
|
+
found = gate.open_issues("agentculture_demo", {"pullRequest": "12"}, "secret", fetch)
|
|
73
|
+
assert [i["line"] for i in found] == [1]
|
|
74
|
+
url, token = calls[0]
|
|
75
|
+
assert url.startswith("https://sonarcloud.io/api/issues/search?")
|
|
76
|
+
assert "componentKeys=agentculture_demo" in url
|
|
77
|
+
assert "pullRequest=12" in url
|
|
78
|
+
assert token == "secret"
|
|
79
|
+
assert "secret" not in url
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_every_page_is_read() -> None:
|
|
83
|
+
first = {
|
|
84
|
+
"total": gate.PAGE_SIZE + 2,
|
|
85
|
+
"issues": [issue("OPEN", n) for n in range(gate.PAGE_SIZE)],
|
|
86
|
+
}
|
|
87
|
+
second = {"total": gate.PAGE_SIZE + 2, "issues": [issue("OPEN", 900), issue("CONFIRMED", 901)]}
|
|
88
|
+
fetch, calls = fake([first, second])
|
|
89
|
+
found = gate.open_issues("k", {"branch": "main"}, None, fetch)
|
|
90
|
+
assert len(found) == gate.PAGE_SIZE + 2
|
|
91
|
+
assert len(calls) == 2
|
|
92
|
+
assert "p=2" in calls[1][0]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_an_error_answer_is_a_check_error_not_a_pass() -> None:
|
|
96
|
+
fetch, _ = fake([{"errors": [{"msg": "Component key not found"}]}])
|
|
97
|
+
with pytest.raises(gate.CheckError):
|
|
98
|
+
gate.open_issues("k", {"branch": "main"}, None, fetch)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_requests_outside_sonarcloud_are_refused() -> None:
|
|
102
|
+
with pytest.raises(gate.CheckError):
|
|
103
|
+
gate.fetch_json("http://127.0.0.1:9/api/issues/search", None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run(monkeypatch, capsys, pages, argv, token="tok-do-not-print"):
|
|
107
|
+
fetch, _ = fake(pages)
|
|
108
|
+
monkeypatch.setattr(gate, "fetch_json", fetch)
|
|
109
|
+
if token:
|
|
110
|
+
monkeypatch.setenv("SONAR_TOKEN", token)
|
|
111
|
+
real = gate.open_issues
|
|
112
|
+
monkeypatch.setattr(gate, "open_issues", lambda k, s, t: real(k, s, t, fetch))
|
|
113
|
+
code = gate.main(argv)
|
|
114
|
+
out = capsys.readouterr()
|
|
115
|
+
assert token not in out.out + out.err
|
|
116
|
+
return code, out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_exit_0_when_nothing_is_open(monkeypatch, capsys) -> None:
|
|
120
|
+
code, out = run(monkeypatch, capsys, [{"total": 1, "issues": [issue("ACCEPTED")]}],
|
|
121
|
+
["--pull-request", "7"]) # fmt: skip
|
|
122
|
+
assert code == 0
|
|
123
|
+
assert "no open issue on pull request 7" in out.out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_exit_1_lists_each_open_issue(monkeypatch, capsys) -> None:
|
|
127
|
+
pages = [{"total": 2, "issues": [issue("OPEN", 11), issue("CONFIRMED", 22)]}]
|
|
128
|
+
code, out = run(monkeypatch, capsys, pages, ["--branch", "main"])
|
|
129
|
+
assert code == 1
|
|
130
|
+
assert "2 open issue(s) on branch main" in out.err
|
|
131
|
+
assert "tests/test_x.py:11" in out.err
|
|
132
|
+
assert "tests/test_x.py:22" in out.err
|
|
133
|
+
assert "Accepted" in out.err
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_exit_2_when_the_check_cannot_be_made(monkeypatch, capsys) -> None:
|
|
137
|
+
code, out = run(monkeypatch, capsys, [{"errors": [{"msg": "nope"}]}], ["--branch", "main"])
|
|
138
|
+
assert code == 2
|
|
139
|
+
assert "could not check branch main" in out.err
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_the_workflow_runs_the_check_after_the_scan() -> None:
|
|
143
|
+
text = (REPO_ROOT / ".github" / "workflows" / "tests.yml").read_text(encoding="utf-8")
|
|
144
|
+
scan = text.index("SonarCloud Scan")
|
|
145
|
+
check = text.index("scripts/sonar_open_issues.py")
|
|
146
|
+
assert scan < check
|
|
147
|
+
# Guarded like the scan itself: no token, no scan, no check.
|
|
148
|
+
step = text[text.rindex("- name:", 0, check) : check]
|
|
149
|
+
assert "env.SONAR_TOKEN != ''" in step
|
|
150
|
+
assert "sonar.qualitygate.wait=true" in (REPO_ROOT / "sonar-project.properties").read_text(
|
|
151
|
+
encoding="utf-8"
|
|
152
|
+
)
|