claude-dev-env 8.6.0 → 8.7.0
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.
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +4 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +155 -18
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +130 -0
- package/bin/install.test.mjs +25 -0
- package/docs/high-trust-agent-delivery.md +132 -0
- package/hooks/advisory/refactor_guard.py +2 -2
- package/hooks/advisory/test_refactor_guard.py +21 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
- package/hooks/blocking/code_rules_shared.py +4 -7
- package/hooks/blocking/conftest.py +22 -0
- package/hooks/blocking/state_description_blocker.py +232 -90
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +27 -54
- package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
- package/hooks/blocking/test_code_rules_shared.py +18 -0
- package/hooks/blocking/test_conftest.py +35 -0
- package/hooks/blocking/test_state_description_blocker.py +59 -0
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
- package/hooks/git-hooks/pull_request_handoff.py +2 -2
- package/hooks/git-hooks/test_config.py +10 -3
- package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
- package/hooks/git-hooks/test_verification_notice_state.py +50 -0
- package/hooks/git-hooks/verification_notice_state.py +3 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
- package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
- package/hooks/hooks_constants/state_description_blocker_constants.py +2 -1
- package/hooks/hooks_constants/test_state_description_blocker_constants.py +87 -0
- package/hooks/validators/exempt_paths.py +3 -18
- package/hooks/validators/test_exempt_paths.py +20 -0
- package/hooks/validators/test_python_antipattern_checks.py +14 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +54 -0
- package/rules/archiving-agent-config.md +10 -0
- package/rules/code-standards.md +1 -0
- package/rules/correction-lens.md +134 -0
- package/rules/flag-non-breaking-findings.md +11 -0
- package/rules/no-contrast-framing.md +68 -0
- package/rules/review-closure-is-a-check.md +44 -0
- package/scripts/Invoke-PesterSuites.ps1 +42 -0
- package/scripts/agent_merge_check.py +381 -0
- package/scripts/check.ps1 +12 -1
- package/scripts/ci/install-playtest.mjs +291 -0
- package/scripts/ci/install-playtest.test.mjs +84 -0
- package/scripts/ci/installer-lifecycle.mjs +11 -30
- package/scripts/ci/installer-lifecycle.test.mjs +14 -0
- package/scripts/ci/pstack-command-shim.test.mjs +33 -0
- package/scripts/ci/scratch-home-install.mjs +73 -0
- package/scripts/ci/scratch-home-install.test.mjs +49 -0
- package/scripts/codex_compat_materializer.py +33 -16
- package/scripts/contrast_framing.py +101 -0
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
- package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
- package/scripts/durable_post_lint.py +30 -4
- package/scripts/followup_cli.py +6 -10
- package/scripts/merge_gate_checks.py +236 -0
- package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
- package/scripts/policy_lint/adapter_detectors.py +1 -1
- package/scripts/policy_lint/adapter_support.py +3 -14
- package/scripts/policy_lint/adapters.py +30 -1
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/repository_checks/config/constants.py +12 -0
- package/scripts/repository_checks/config/policy_document.py +28 -2
- package/scripts/repository_checks/followups.py +54 -0
- package/scripts/repository_checks/match_exemptions.py +67 -0
- package/scripts/repository_checks/models.py +46 -2
- package/scripts/repository_checks/reporting.py +23 -7
- package/scripts/repository_checks/tracked_secrets.py +2 -2
- package/scripts/repository_policy.py +4 -2
- package/scripts/review_closure.py +134 -0
- package/scripts/review_closure_github.py +345 -0
- package/scripts/review_closure_model.py +360 -0
- package/scripts/test_agent_merge_check.py +317 -0
- package/scripts/test_durable_post_lint.py +46 -0
- package/scripts/test_followup_cli.py +40 -0
- package/scripts/test_merge_gate_checks.py +92 -0
- package/scripts/tests/test_code_review_constants.py +78 -0
- package/scripts/tests/test_codex_compat_materializer.py +48 -4
- package/scripts/tests/test_contrast_framing.py +117 -0
- package/scripts/tests/test_followups.py +88 -0
- package/scripts/tests/test_match_exemptions.py +291 -0
- package/scripts/tests/test_models.py +67 -2
- package/scripts/tests/test_package_inventory.py +6 -4
- package/scripts/tests/test_pester_suites_run.py +53 -0
- package/scripts/tests/test_policy_document.py +39 -1
- package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +4 -1
- package/scripts/tests/test_reporting.py +18 -0
- package/scripts/tests/test_repository_policy.py +43 -1
- package/scripts/tests/test_review_closure.py +126 -0
- package/scripts/tests/test_review_closure_github.py +240 -0
- package/scripts/tests/test_review_closure_model.py +299 -0
- package/scripts/tests/test_tracked_secrets.py +42 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
- package/scripts/repository_checks/email_exemptions.py +0 -39
- package/scripts/tests/test_email_exemptions.py +0 -158
|
@@ -49,7 +49,7 @@ Review each hit. Ignore:
|
|
|
49
49
|
|
|
50
50
|
- Stage only clean files
|
|
51
51
|
- Prefer `--body-file` for `gh` posts (also required by the gh-cli-conventions rule)
|
|
52
|
-
-
|
|
52
|
+
- No hook re-scans the next Write, commit, or post; `pii_prevention_blocker` was retired. Re-run this sweep before the next commit or post.
|
|
53
53
|
|
|
54
54
|
## Accepted residual (do not over-scrub)
|
|
55
55
|
|
|
@@ -76,6 +76,7 @@ from advisor_scripts_constants.model_tier_run_validator_constants import (
|
|
|
76
76
|
CLI_SUCCESS_EXIT_CODE,
|
|
77
77
|
CLI_USAGE_MESSAGE,
|
|
78
78
|
CLI_VALIDATION_FAILURE_EXIT_CODE,
|
|
79
|
+
EVIDENCE_MUST_BE_OBJECT_MESSAGE,
|
|
79
80
|
HOST_PROFILE_CLAUDE,
|
|
80
81
|
HOST_PROFILE_CODEX,
|
|
81
82
|
HOST_PROFILE_JSON_KEY,
|
|
@@ -242,7 +243,7 @@ def _validate_advisor_evidence(
|
|
|
242
243
|
run: ModelTierRun,
|
|
243
244
|
) -> None:
|
|
244
245
|
if not isinstance(evidence, Mapping):
|
|
245
|
-
raise ModelTierRunError(
|
|
246
|
+
raise ModelTierRunError(EVIDENCE_MUST_BE_OBJECT_MESSAGE)
|
|
246
247
|
raw_schema_version = evidence.get("schema_version")
|
|
247
248
|
if isinstance(raw_schema_version, bool) or raw_schema_version != 1:
|
|
248
249
|
raise ModelTierRunError("evidence.schema_version must be 1")
|
|
@@ -415,7 +416,7 @@ def load_model_tier_run_from_json_path(from_path: Path) -> ModelTierRun:
|
|
|
415
416
|
raise TypeError(HOST_PROFILE_MUST_BE_STRING_MESSAGE)
|
|
416
417
|
raw_evidence = parsed_payload.get("evidence")
|
|
417
418
|
if raw_evidence is not None and not isinstance(raw_evidence, dict):
|
|
418
|
-
raise TypeError(
|
|
419
|
+
raise TypeError(EVIDENCE_MUST_BE_OBJECT_MESSAGE)
|
|
419
420
|
return ModelTierRun(
|
|
420
421
|
own_tier=parsed_payload["own_tier"],
|
|
421
422
|
candidate_tiers=list(parsed_payload["candidate_tiers"]),
|
|
@@ -35,6 +35,9 @@ load_model_tier_run_from_json_path = (
|
|
|
35
35
|
)
|
|
36
36
|
ADVISOR_MODEL_TIER = model_tier_run_validator.ADVISOR_MODEL_TIER
|
|
37
37
|
CODEX_BIND_SUCCESS_TOKEN = model_tier_run_validator.CODEX_BIND_SUCCESS_TOKEN
|
|
38
|
+
EVIDENCE_MUST_BE_OBJECT_MESSAGE = (
|
|
39
|
+
model_tier_run_validator.EVIDENCE_MUST_BE_OBJECT_MESSAGE
|
|
40
|
+
)
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
def _native_codex_evidence(
|
|
@@ -136,6 +139,38 @@ def test_cli_reads_versioned_advisor_evidence(tmp_path: Path) -> None:
|
|
|
136
139
|
assert loaded_run.evidence["consult"]["report_back_status"] == "recorded"
|
|
137
140
|
|
|
138
141
|
|
|
142
|
+
def test_non_object_evidence_reports_the_same_message_at_validate_and_load(
|
|
143
|
+
tmp_path: Path,
|
|
144
|
+
) -> None:
|
|
145
|
+
run = ModelTierRun(
|
|
146
|
+
own_tier="Opus",
|
|
147
|
+
candidate_tiers=[ADVISOR_MODEL_TIER],
|
|
148
|
+
attempts=[{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
|
|
149
|
+
selected_tier=ADVISOR_MODEL_TIER,
|
|
150
|
+
host_profile="Codex",
|
|
151
|
+
evidence=["not", "an", "object"],
|
|
152
|
+
)
|
|
153
|
+
with pytest.raises(ModelTierRunError, match=EVIDENCE_MUST_BE_OBJECT_MESSAGE):
|
|
154
|
+
validate_model_tier_run(run)
|
|
155
|
+
|
|
156
|
+
log_path = tmp_path / "model-tier-run.json"
|
|
157
|
+
log_path.write_text(
|
|
158
|
+
json.dumps(
|
|
159
|
+
{
|
|
160
|
+
"own_tier": "Opus",
|
|
161
|
+
"candidate_tiers": [ADVISOR_MODEL_TIER],
|
|
162
|
+
"attempts": [{"tier": ADVISOR_MODEL_TIER, "result": "spawned"}],
|
|
163
|
+
"selected_tier": ADVISOR_MODEL_TIER,
|
|
164
|
+
"host_profile": "Codex",
|
|
165
|
+
"evidence": "not an object",
|
|
166
|
+
}
|
|
167
|
+
),
|
|
168
|
+
encoding="utf-8",
|
|
169
|
+
)
|
|
170
|
+
with pytest.raises(TypeError, match=EVIDENCE_MUST_BE_OBJECT_MESSAGE):
|
|
171
|
+
load_model_tier_run_from_json_path(from_path=log_path)
|
|
172
|
+
|
|
173
|
+
|
|
139
174
|
def test_clean_single_spawn_at_top_of_slice_passes() -> None:
|
|
140
175
|
run = ModelTierRun(
|
|
141
176
|
own_tier="Opus",
|
|
@@ -10,6 +10,10 @@ MARKDOWN_FILE_EXTENSION: str = ".md"
|
|
|
10
10
|
|
|
11
11
|
INLINE_CODE_SPAN_PATTERN: re.Pattern[str] = re.compile(r"`[^`]*`")
|
|
12
12
|
|
|
13
|
+
MARKDOWN_LINK_TARGET_PATTERN: re.Pattern[str] = re.compile(r"\]\([^)]*\)")
|
|
14
|
+
|
|
15
|
+
SENTENCE_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"(?<=[.!?])\s+")
|
|
16
|
+
|
|
13
17
|
SNAKE_CASE_IDENTIFIER_PATTERN: re.Pattern[str] = re.compile(
|
|
14
18
|
r"\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b"
|
|
15
19
|
)
|
|
@@ -62,10 +62,12 @@ from pr_loop_shared_constants.terminology_sweep_constants import (
|
|
|
62
62
|
JAVASCRIPT_LINE_COMMENT_MARKER,
|
|
63
63
|
JSDOC_CONTINUATION_MARKER,
|
|
64
64
|
MARKDOWN_FILE_EXTENSION,
|
|
65
|
+
MARKDOWN_LINK_TARGET_PATTERN,
|
|
65
66
|
MINIMUM_IDENTIFIER_TOKEN_COUNT,
|
|
66
67
|
PROSE_WINDOW_WORD_SEPARATOR,
|
|
67
68
|
PROSE_WORD_PATTERN,
|
|
68
69
|
PYTHON_COMMENT_MARKER,
|
|
70
|
+
SENTENCE_BOUNDARY_PATTERN,
|
|
69
71
|
SNAKE_CASE_IDENTIFIER_PATTERN,
|
|
70
72
|
STRING_LITERAL_CONTENT_PATTERN,
|
|
71
73
|
TERMINOLOGY_FINDING_TEMPLATE,
|
|
@@ -74,7 +76,6 @@ from pr_loop_shared_constants.terminology_sweep_constants import (
|
|
|
74
76
|
TEST_FILE_PREFIX,
|
|
75
77
|
TEST_FILE_SUFFIX,
|
|
76
78
|
)
|
|
77
|
-
|
|
78
79
|
from subprocess_window_access import hidden_window_creation_flags
|
|
79
80
|
|
|
80
81
|
IdentifierTuple = tuple[str, ...]
|
|
@@ -161,27 +162,76 @@ def _collect_introduced_identifiers(
|
|
|
161
162
|
) -> frozenset[IdentifierTuple]:
|
|
162
163
|
"""Return the identifier tuples introduced on added code lines.
|
|
163
164
|
|
|
165
|
+
A name bound only inside a test module is a fixture's own local variable,
|
|
166
|
+
not a term the rest of the repository is expected to agree with, so a
|
|
167
|
+
test file's identifiers are left out.
|
|
168
|
+
|
|
164
169
|
Args:
|
|
165
170
|
all_added_lines: Added-line triples from :func:`_parse_added_lines`.
|
|
166
171
|
|
|
167
172
|
Returns:
|
|
168
|
-
Every multi-word identifier tuple appearing on an added
|
|
173
|
+
Every multi-word identifier tuple appearing on an added,
|
|
174
|
+
non-test-module code line.
|
|
169
175
|
"""
|
|
170
176
|
all_identifier_tuples: set[IdentifierTuple] = set()
|
|
171
177
|
for each_file_path, _, each_text in all_added_lines:
|
|
172
|
-
if
|
|
178
|
+
if _is_swept_non_test_code_file(each_file_path):
|
|
173
179
|
all_identifier_tuples.update(_identifier_tuples_in_text(each_text))
|
|
174
180
|
return frozenset(all_identifier_tuples)
|
|
175
181
|
|
|
176
182
|
|
|
183
|
+
def _fragments_split_at_breaks(all_fragments: list[str]) -> list[str]:
|
|
184
|
+
"""Split each fragment at every sentence, link, and code-span break in turn."""
|
|
185
|
+
for each_break_pattern in (
|
|
186
|
+
INLINE_CODE_SPAN_PATTERN,
|
|
187
|
+
MARKDOWN_LINK_TARGET_PATTERN,
|
|
188
|
+
SENTENCE_BOUNDARY_PATTERN,
|
|
189
|
+
):
|
|
190
|
+
all_fragments = [
|
|
191
|
+
each_piece
|
|
192
|
+
for each_fragment in all_fragments
|
|
193
|
+
for each_piece in each_break_pattern.split(each_fragment)
|
|
194
|
+
]
|
|
195
|
+
return all_fragments
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _split_into_prose_fragments(raw_fragment: str) -> list[str]:
|
|
199
|
+
"""Split a raw prose fragment at every sentence, link, and code-span break.
|
|
200
|
+
|
|
201
|
+
::
|
|
202
|
+
|
|
203
|
+
raw: "…gates the pull request. [`link`](docs/README.md) explains it."
|
|
204
|
+
split -> ["…gates the pull request", "explains it."]
|
|
205
|
+
flag: a window built from the joined text -- "pull request docs"
|
|
206
|
+
ok: "pull request" and "docs" now sit in different fragments
|
|
207
|
+
|
|
208
|
+
A sentence-ending period, a Markdown link's target, and a backticked code
|
|
209
|
+
span each end whatever the reader was reading and start something new.
|
|
210
|
+
Splitting there keeps a word window scoped to one sentence, clear of a
|
|
211
|
+
stripped span's unrelated text on its far side.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
raw_fragment: One prose fragment collected from an added line.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
The fragment's pieces with every break removed, in order, dropping
|
|
218
|
+
any piece left blank.
|
|
219
|
+
"""
|
|
220
|
+
all_fragments = _fragments_split_at_breaks([raw_fragment])
|
|
221
|
+
return [
|
|
222
|
+
each_fragment.strip() for each_fragment in all_fragments if each_fragment.strip()
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
177
226
|
def _prose_fragments(file_path: str, line_text: str) -> list[str]:
|
|
178
227
|
"""Return the prose fragments of an added line worth scanning for terms.
|
|
179
228
|
|
|
180
|
-
A Markdown line is prose
|
|
181
|
-
backticked span names code verbatim
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
229
|
+
A Markdown line is prose split at its sentence, link, and code-span
|
|
230
|
+
breaks, since a backticked span names code verbatim and a link target is
|
|
231
|
+
a path, not a word. A code line contributes its comment tail, its JSDoc
|
|
232
|
+
continuation text, and the contents of its string literals, each split
|
|
233
|
+
the same way. A test module contributes its comment tail and JSDoc text
|
|
234
|
+
only. Its string literals hold fixture data, not prose.
|
|
185
235
|
|
|
186
236
|
Args:
|
|
187
237
|
file_path: The path the added line belongs to.
|
|
@@ -192,17 +242,21 @@ def _prose_fragments(file_path: str, line_text: str) -> list[str]:
|
|
|
192
242
|
"""
|
|
193
243
|
extension = _file_extension(file_path)
|
|
194
244
|
if extension == MARKDOWN_FILE_EXTENSION:
|
|
195
|
-
return
|
|
245
|
+
return _split_into_prose_fragments(line_text)
|
|
196
246
|
if extension not in ALL_SWEEP_CODE_FILE_EXTENSIONS:
|
|
197
247
|
return []
|
|
198
|
-
|
|
248
|
+
all_raw_fragments: list[str] = []
|
|
199
249
|
stripped_line = line_text.strip()
|
|
200
250
|
if stripped_line.startswith(JSDOC_CONTINUATION_MARKER):
|
|
201
|
-
|
|
202
|
-
|
|
251
|
+
all_raw_fragments.append(stripped_line)
|
|
252
|
+
all_raw_fragments.extend(_comment_fragments(line_text))
|
|
203
253
|
if not _is_test_file(file_path):
|
|
204
|
-
|
|
205
|
-
return
|
|
254
|
+
all_raw_fragments.extend(_string_literal_fragments(line_text))
|
|
255
|
+
return [
|
|
256
|
+
each_split_fragment
|
|
257
|
+
for each_raw_fragment in all_raw_fragments
|
|
258
|
+
for each_split_fragment in _split_into_prose_fragments(each_raw_fragment)
|
|
259
|
+
]
|
|
206
260
|
|
|
207
261
|
|
|
208
262
|
def _is_test_file(file_path: str) -> bool:
|
|
@@ -245,6 +299,13 @@ def _is_test_file(file_path: str) -> bool:
|
|
|
245
299
|
)
|
|
246
300
|
|
|
247
301
|
|
|
302
|
+
def _is_swept_non_test_code_file(file_path: str) -> bool:
|
|
303
|
+
"""Return whether a diff path names a swept code file outside a test module."""
|
|
304
|
+
return _file_extension(
|
|
305
|
+
file_path
|
|
306
|
+
) in ALL_SWEEP_CODE_FILE_EXTENSIONS and not _is_test_file(file_path)
|
|
307
|
+
|
|
308
|
+
|
|
248
309
|
def _comment_fragments(line_text: str) -> list[str]:
|
|
249
310
|
"""Return the comment tail of a code line, or an empty list when absent."""
|
|
250
311
|
all_fragments: list[str] = []
|
|
@@ -428,15 +489,30 @@ def _near_miss_identifier(
|
|
|
428
489
|
all_identifier_tuples: frozenset[IdentifierTuple],
|
|
429
490
|
identifiers_by_first_token: dict[str, list[IdentifierTuple]],
|
|
430
491
|
all_identifier_tokens: frozenset[str],
|
|
492
|
+
all_known_identifier_tuples: frozenset[IdentifierTuple],
|
|
431
493
|
) -> IdentifierTuple | None:
|
|
432
494
|
"""Return an identifier the candidate near-misses, or None when it does not.
|
|
433
495
|
|
|
496
|
+
::
|
|
497
|
+
|
|
498
|
+
known identifier: cde_lint (scripts/cde_lint.py, the tool's own name)
|
|
499
|
+
introduced: cde_lint_path 3-token, adds "path" to that name
|
|
500
|
+
candidate: "cde lint standard" shares "cde lint", then "standard"
|
|
501
|
+
candidate[:-1]: ("cde", "lint") == the known identifier cde_lint
|
|
502
|
+
ok: candidate[:-1] names cde_lint verbatim -- agreement, not a near-miss
|
|
503
|
+
flag: "premium request budget" against premium_request_interactions
|
|
504
|
+
-- "premium request" names no identifier of its own
|
|
505
|
+
|
|
434
506
|
Args:
|
|
435
507
|
candidate_tuple: The prose term's lowercase token tuple.
|
|
436
508
|
all_identifier_tuples: Every introduced identifier tuple.
|
|
437
509
|
identifiers_by_first_token: Identifier tuples grouped by leading token.
|
|
438
510
|
all_identifier_tokens: Every token of every identifier on added code
|
|
439
511
|
lines, used to spare prose built from real code vocabulary.
|
|
512
|
+
all_known_identifier_tuples: Every identifier tuple the sweep knows
|
|
513
|
+
about, introduced or pre-existing, plus each added file's stem,
|
|
514
|
+
used to spare a candidate whose leading tokens name one of them
|
|
515
|
+
verbatim.
|
|
440
516
|
|
|
441
517
|
Returns:
|
|
442
518
|
The first identifier the candidate renames in only its final token. Such a
|
|
@@ -449,9 +525,12 @@ def _near_miss_identifier(
|
|
|
449
525
|
common English compound tail word (``read-only``, ``data-driven``); it
|
|
450
526
|
contains an English stopword (``to a``, ``each image``); it differs
|
|
451
527
|
from the identifier only by a singular/plural form of one or more
|
|
452
|
-
tokens (``test files`` against ``test_file``);
|
|
528
|
+
tokens (``test files`` against ``test_file``); every diverging word
|
|
453
529
|
is itself a token of some introduced identifier (``target width box``
|
|
454
|
-
when ``box_height`` is also in the diff)
|
|
530
|
+
when ``box_height`` is also in the diff); or its leading tokens, with
|
|
531
|
+
the final diverging word set aside, spell out a known identifier of
|
|
532
|
+
their own (``cde_lint standard`` against ``cde_lint_path``, when
|
|
533
|
+
``cde_lint`` already names something in the tree).
|
|
455
534
|
"""
|
|
456
535
|
if not candidate_tuple or candidate_tuple in all_identifier_tuples:
|
|
457
536
|
return None
|
|
@@ -468,6 +547,8 @@ def _near_miss_identifier(
|
|
|
468
547
|
continue
|
|
469
548
|
if not _diverges_only_in_final_token(candidate_tuple, each_identifier):
|
|
470
549
|
continue
|
|
550
|
+
if candidate_tuple[:-1] in all_known_identifier_tuples:
|
|
551
|
+
continue
|
|
471
552
|
diverging_words = [
|
|
472
553
|
each_word
|
|
473
554
|
for each_word, each_token in zip(candidate_tuple, each_identifier)
|
|
@@ -489,6 +570,7 @@ def _findings_for_line(
|
|
|
489
570
|
all_identifier_tuples: frozenset[IdentifierTuple],
|
|
490
571
|
identifiers_by_first_token: dict[str, list[IdentifierTuple]],
|
|
491
572
|
all_identifier_tokens: frozenset[str],
|
|
573
|
+
all_known_identifier_tuples: frozenset[IdentifierTuple],
|
|
492
574
|
) -> list[str]:
|
|
493
575
|
"""Return the near-miss findings for one added line.
|
|
494
576
|
|
|
@@ -500,6 +582,8 @@ def _findings_for_line(
|
|
|
500
582
|
identifiers_by_first_token: Identifier tuples grouped by leading token.
|
|
501
583
|
all_identifier_tokens: Every token of every identifier on added code
|
|
502
584
|
lines, used to spare prose built from real code vocabulary.
|
|
585
|
+
all_known_identifier_tuples: Every identifier tuple the sweep knows
|
|
586
|
+
about, passed through to :func:`_near_miss_identifier`.
|
|
503
587
|
|
|
504
588
|
Returns:
|
|
505
589
|
One finding string per distinct near-miss term on the line.
|
|
@@ -515,6 +599,7 @@ def _findings_for_line(
|
|
|
515
599
|
all_identifier_tuples,
|
|
516
600
|
identifiers_by_first_token,
|
|
517
601
|
all_identifier_tokens,
|
|
602
|
+
all_known_identifier_tuples,
|
|
518
603
|
)
|
|
519
604
|
if matched_identifier is None or each_tuple in reported_tuples:
|
|
520
605
|
continue
|
|
@@ -530,6 +615,48 @@ def _findings_for_line(
|
|
|
530
615
|
return all_findings
|
|
531
616
|
|
|
532
617
|
|
|
618
|
+
def _stem_identifier_tuple_for_code_file(each_file_path: str) -> IdentifierTuple | None:
|
|
619
|
+
"""Return a non-test code file's stem as a multi-word identifier tuple.
|
|
620
|
+
|
|
621
|
+
``None`` when the path is not a swept code file, is a test file, or its
|
|
622
|
+
stem carries fewer than the minimum identifier token count.
|
|
623
|
+
"""
|
|
624
|
+
if not _is_swept_non_test_code_file(each_file_path):
|
|
625
|
+
return None
|
|
626
|
+
stem_tuple = _identifier_token_tuple(Path(each_file_path).stem)
|
|
627
|
+
if len(stem_tuple) < MINIMUM_IDENTIFIER_TOKEN_COUNT:
|
|
628
|
+
return None
|
|
629
|
+
return stem_tuple
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _file_stem_identifier_tuples(
|
|
633
|
+
all_added_lines: list[tuple[str, int, str]],
|
|
634
|
+
) -> frozenset[IdentifierTuple]:
|
|
635
|
+
"""Return the identifier tuple of each added, non-test code file's stem.
|
|
636
|
+
|
|
637
|
+
::
|
|
638
|
+
|
|
639
|
+
touched file: scripts/cde_lint_partition.py
|
|
640
|
+
stem tuple: ("cde", "lint", "partition")
|
|
641
|
+
|
|
642
|
+
A file that exists in the tree names something whether or not that name
|
|
643
|
+
is ever spelled out as a code identifier, so a file's stem joins the
|
|
644
|
+
known-identifier vocabulary the same way a spelled-out name does.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
all_added_lines: Added-line triples from :func:`_parse_added_lines`.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
One tuple per distinct, multi-word stem of an added, non-test code
|
|
651
|
+
file.
|
|
652
|
+
"""
|
|
653
|
+
return frozenset(
|
|
654
|
+
stem_tuple
|
|
655
|
+
for each_file_path, _, _ in all_added_lines
|
|
656
|
+
if (stem_tuple := _stem_identifier_tuple_for_code_file(each_file_path)) is not None
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
533
660
|
def _find_terminology_near_misses(
|
|
534
661
|
diff_text: str, all_preexisting_identifier_tuples: frozenset[IdentifierTuple]) -> list[str]:
|
|
535
662
|
all_added_lines = _parse_added_lines(diff_text)
|
|
@@ -537,6 +664,9 @@ def _find_terminology_near_misses(
|
|
|
537
664
|
all_identifier_tokens = frozenset(
|
|
538
665
|
each_token for each_tuple in all_identifier_tuples for each_token in each_tuple
|
|
539
666
|
)
|
|
667
|
+
all_known_identifier_tuples = all_identifier_tuples | _file_stem_identifier_tuples(
|
|
668
|
+
all_added_lines
|
|
669
|
+
)
|
|
540
670
|
introduced_tuples = frozenset(
|
|
541
671
|
each_tuple
|
|
542
672
|
for each_tuple in all_identifier_tuples
|
|
@@ -557,6 +687,7 @@ def _find_terminology_near_misses(
|
|
|
557
687
|
introduced_tuples,
|
|
558
688
|
introduced_by_first_token,
|
|
559
689
|
all_identifier_tokens,
|
|
690
|
+
all_known_identifier_tuples,
|
|
560
691
|
)
|
|
561
692
|
)
|
|
562
693
|
return all_findings
|
|
@@ -605,15 +736,21 @@ def repository_environment() -> dict[str, str]:
|
|
|
605
736
|
def _identifier_names_on_added_code_lines(diff_text: str) -> frozenset[str]:
|
|
606
737
|
"""Return every multi-word identifier name on the diff's added code lines.
|
|
607
738
|
|
|
739
|
+
A name bound only inside a test module is left out, matching
|
|
740
|
+
:func:`_collect_introduced_identifiers`, since it is never treated as
|
|
741
|
+
introduced terminology and its presence in the base tree is not worth a
|
|
742
|
+
lookup.
|
|
743
|
+
|
|
608
744
|
Args:
|
|
609
745
|
diff_text: The unified-diff text to scan.
|
|
610
746
|
|
|
611
747
|
Returns:
|
|
612
|
-
The distinct snake_case and camelCase names of two or more tokens
|
|
748
|
+
The distinct snake_case and camelCase names of two or more tokens
|
|
749
|
+
on a non-test-module code line.
|
|
613
750
|
"""
|
|
614
751
|
all_names: set[str] = set()
|
|
615
752
|
for each_file_path, _, each_text in _parse_added_lines(diff_text):
|
|
616
|
-
if
|
|
753
|
+
if not _is_swept_non_test_code_file(each_file_path):
|
|
617
754
|
continue
|
|
618
755
|
all_found_names = SNAKE_CASE_IDENTIFIER_PATTERN.findall(each_text)
|
|
619
756
|
all_found_names += CAMEL_CASE_IDENTIFIER_PATTERN.findall(each_text)
|
|
@@ -626,6 +626,123 @@ def test_does_not_flag_two_token_identifier_first_word_coincidence() -> None:
|
|
|
626
626
|
assert sweep_diff(diff) == []
|
|
627
627
|
|
|
628
628
|
|
|
629
|
+
def test_does_not_flag_window_crossing_a_sentence_boundary() -> None:
|
|
630
|
+
diff = (
|
|
631
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
632
|
+
"--- a/api/quota.py\n"
|
|
633
|
+
"+++ a/api/quota.py\n"
|
|
634
|
+
"@@ -0,0 +1,1 @@\n"
|
|
635
|
+
"+pull_request_owner = 5\n"
|
|
636
|
+
"diff --git a/README.md b/README.md\n"
|
|
637
|
+
"--- a/README.md\n"
|
|
638
|
+
"+++ b/README.md\n"
|
|
639
|
+
"@@ -0,0 +1,1 @@\n"
|
|
640
|
+
"+Merge the pull request. Trigger warnings surface elsewhere.\n"
|
|
641
|
+
)
|
|
642
|
+
assert sweep_diff(diff) == []
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def test_does_not_flag_window_crossing_a_markdown_link_target() -> None:
|
|
646
|
+
diff = (
|
|
647
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
648
|
+
"--- a/api/quota.py\n"
|
|
649
|
+
"+++ a/api/quota.py\n"
|
|
650
|
+
"@@ -0,0 +1,1 @@\n"
|
|
651
|
+
"+pull_request_owner = 5\n"
|
|
652
|
+
"diff --git a/README.md b/README.md\n"
|
|
653
|
+
"--- a/README.md\n"
|
|
654
|
+
"+++ b/README.md\n"
|
|
655
|
+
"@@ -0,0 +1,1 @@\n"
|
|
656
|
+
"+See the pull request [`link`](docs/README.md) explains it.\n"
|
|
657
|
+
)
|
|
658
|
+
assert sweep_diff(diff) == []
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def test_does_not_flag_identifier_bound_only_in_a_test_module() -> None:
|
|
662
|
+
diff = (
|
|
663
|
+
"diff --git a/tests/test_config.py b/tests/test_config.py\n"
|
|
664
|
+
"--- a/tests/test_config.py\n"
|
|
665
|
+
"+++ b/tests/test_config.py\n"
|
|
666
|
+
"@@ -0,0 +1,1 @@\n"
|
|
667
|
+
"+jon_pc_json = load_fixture()\n"
|
|
668
|
+
"diff --git a/README.md b/README.md\n"
|
|
669
|
+
"--- a/README.md\n"
|
|
670
|
+
"+++ b/README.md\n"
|
|
671
|
+
"@@ -0,0 +1,1 @@\n"
|
|
672
|
+
"+The jon pc export runs first.\n"
|
|
673
|
+
)
|
|
674
|
+
assert sweep_diff(diff) == []
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def test_still_flags_identifier_bound_in_a_production_module() -> None:
|
|
678
|
+
diff = (
|
|
679
|
+
"diff --git a/api/config.py b/api/config.py\n"
|
|
680
|
+
"--- a/api/config.py\n"
|
|
681
|
+
"+++ b/api/config.py\n"
|
|
682
|
+
"@@ -0,0 +1,1 @@\n"
|
|
683
|
+
"+jon_pc_json = load_config()\n"
|
|
684
|
+
"diff --git a/README.md b/README.md\n"
|
|
685
|
+
"--- a/README.md\n"
|
|
686
|
+
"+++ b/README.md\n"
|
|
687
|
+
"@@ -0,0 +1,1 @@\n"
|
|
688
|
+
"+The jon pc export runs first.\n"
|
|
689
|
+
)
|
|
690
|
+
findings = sweep_diff(diff)
|
|
691
|
+
assert len(findings) == 1
|
|
692
|
+
assert "jon pc export" in findings[0]
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def test_does_not_flag_window_whose_leading_tokens_are_a_known_identifier() -> None:
|
|
696
|
+
diff = (
|
|
697
|
+
"diff --git a/api/store.py b/api/store.py\n"
|
|
698
|
+
"--- a/api/store.py\n"
|
|
699
|
+
"+++ b/api/store.py\n"
|
|
700
|
+
"@@ -0,0 +1,2 @@\n"
|
|
701
|
+
"+cde_lint_path = Path(value)\n"
|
|
702
|
+
'+HELP_TEXT = "cde_lint standard output"\n'
|
|
703
|
+
)
|
|
704
|
+
assert sweep_diff(diff) == []
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def test_still_flags_window_whose_leading_tokens_name_nothing_known() -> None:
|
|
708
|
+
diff = (
|
|
709
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
710
|
+
"--- a/api/quota.py\n"
|
|
711
|
+
"+++ b/api/quota.py\n"
|
|
712
|
+
"@@ -0,0 +1,1 @@\n"
|
|
713
|
+
"+premium_request_interactions = 5\n"
|
|
714
|
+
"diff --git a/docs/README.md b/docs/README.md\n"
|
|
715
|
+
"--- a/docs/README.md\n"
|
|
716
|
+
"+++ b/docs/README.md\n"
|
|
717
|
+
"@@ -0,0 +1,1 @@\n"
|
|
718
|
+
"+The premium request budget gates the run.\n"
|
|
719
|
+
)
|
|
720
|
+
findings = sweep_diff(diff)
|
|
721
|
+
assert len(findings) == 1
|
|
722
|
+
assert "premium request budget" in findings[0]
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def test_does_not_flag_window_whose_leading_tokens_are_an_added_files_stem() -> None:
|
|
726
|
+
diff = (
|
|
727
|
+
"diff --git a/scripts/cde_lint.py b/scripts/cde_lint.py\n"
|
|
728
|
+
"--- a/scripts/cde_lint.py\n"
|
|
729
|
+
"+++ b/scripts/cde_lint.py\n"
|
|
730
|
+
"@@ -0,0 +1,1 @@\n"
|
|
731
|
+
"+VERSION = 1\n"
|
|
732
|
+
"diff --git a/scripts/cde_lint_partition.py b/scripts/cde_lint_partition.py\n"
|
|
733
|
+
"--- a/scripts/cde_lint_partition.py\n"
|
|
734
|
+
"+++ b/scripts/cde_lint_partition.py\n"
|
|
735
|
+
"@@ -0,0 +1,1 @@\n"
|
|
736
|
+
"+cde_lint_owner = 5\n"
|
|
737
|
+
"diff --git a/docs/README.md b/docs/README.md\n"
|
|
738
|
+
"--- a/docs/README.md\n"
|
|
739
|
+
"+++ b/docs/README.md\n"
|
|
740
|
+
"@@ -0,0 +1,1 @@\n"
|
|
741
|
+
"+Run cde_lint standard checks first.\n"
|
|
742
|
+
)
|
|
743
|
+
assert sweep_diff(diff) == []
|
|
744
|
+
|
|
745
|
+
|
|
629
746
|
def test_flags_near_miss_when_second_leading_token_is_plural_variant() -> None:
|
|
630
747
|
diff = (
|
|
631
748
|
"diff --git a/api/store.py b/api/store.py\n"
|
|
@@ -642,3 +759,16 @@ def test_flags_near_miss_when_second_leading_token_is_plural_variant() -> None:
|
|
|
642
759
|
findings = sweep_diff(diff)
|
|
643
760
|
assert len(findings) == 1
|
|
644
761
|
assert "retry policies limit" in findings[0]
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def test_a_swept_code_file_outside_the_test_modules_reads_as_swept() -> None:
|
|
765
|
+
assert sweep_module._is_swept_non_test_code_file("api/store.py") is True
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def test_a_test_module_does_not_read_as_a_swept_code_file() -> None:
|
|
769
|
+
assert sweep_module._is_swept_non_test_code_file("api/test_store.py") is False
|
|
770
|
+
assert sweep_module._is_swept_non_test_code_file("api/tests/store.py") is False
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def test_a_markdown_document_does_not_read_as_a_swept_code_file() -> None:
|
|
774
|
+
assert sweep_module._is_swept_non_test_code_file("docs/README.md") is False
|
package/bin/install.test.mjs
CHANGED
|
@@ -2928,6 +2928,31 @@ test('retired skills leave install groups and stay in the cleanup registry', ()
|
|
|
2928
2928
|
}
|
|
2929
2929
|
});
|
|
2930
2930
|
|
|
2931
|
+
test('the README agents section names the shipped agent, not archived personal agents', () => {
|
|
2932
|
+
const readme = readFileSync(new URL('../../../README.md', import.meta.url), 'utf8');
|
|
2933
|
+
const agentsSection = readme.slice(
|
|
2934
|
+
readme.indexOf('### Agents'),
|
|
2935
|
+
readme.indexOf('### Commands'),
|
|
2936
|
+
);
|
|
2937
|
+
assert.match(agentsSection, /`poteto-agent`/);
|
|
2938
|
+
for (const archivedAgentName of [
|
|
2939
|
+
'clean-coder',
|
|
2940
|
+
'code-quality-agent',
|
|
2941
|
+
'git-commit-crafter',
|
|
2942
|
+
'issue-tracker',
|
|
2943
|
+
'plan-packet-validator',
|
|
2944
|
+
'pr-description-writer',
|
|
2945
|
+
'session-advisor',
|
|
2946
|
+
'skill-writer-agent',
|
|
2947
|
+
]) {
|
|
2948
|
+
assert.equal(
|
|
2949
|
+
agentsSection.includes(`\`${archivedAgentName}\``),
|
|
2950
|
+
false,
|
|
2951
|
+
`${archivedAgentName} is archived and should not appear as a shipped agent`,
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
|
|
2931
2956
|
test('the README names current session tools', () => {
|
|
2932
2957
|
const readme = readFileSync(new URL('../../../README.md', import.meta.url), 'utf8');
|
|
2933
2958
|
assert.doesNotMatch(readme, /--only journal|session-log|session-tidy/);
|