claude-dev-env 2.35.0 → 4.0.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/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -0
- package/.agents/skills/team-advisor/SKILL.md +11 -11
- package/_shared/advisor/advisor-protocol.md +51 -51
- package/_shared/advisor/reference/advisor-block.md +9 -9
- package/_shared/advisor/reference/astra-rung.md +39 -0
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +6 -6
- package/_shared/advisor/reference/spawn-walk-log.md +9 -9
- package/_shared/advisor/reference/third-party-bind.md +11 -11
- package/_shared/advisor/reference/warm-up.md +13 -13
- package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
- package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
- package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
- package/bin/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/README.md +35 -1
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +368 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +376 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +388 -0
- package/scripts/policy_lint/registry.py +171 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- package/scripts/repository_checks/__init__.py +1 -0
- package/scripts/repository_checks/claude_md.py +108 -0
- package/scripts/repository_checks/config/__init__.py +1 -0
- package/scripts/repository_checks/config/constants.py +150 -0
- package/scripts/repository_checks/env_var_documentation.py +243 -0
- package/scripts/repository_checks/hook_modules.py +34 -0
- package/scripts/repository_checks/models.py +37 -0
- package/scripts/repository_checks/package_inventory.py +118 -0
- package/scripts/repository_checks/pytest_testpaths.py +84 -0
- package/scripts/repository_checks/reporting.py +39 -0
- package/scripts/repository_checks/runner.py +93 -0
- package/scripts/repository_checks/tracked_secrets.py +147 -0
- package/scripts/repository_checks/tracked_tree.py +28 -0
- package/scripts/repository_policy.py +120 -0
- package/scripts/select_pstack_models.mjs +304 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +398 -0
- package/scripts/tests/test_claude_md.py +68 -0
- package/scripts/tests/test_env_var_documentation.py +133 -0
- package/scripts/tests/test_hook_modules.py +17 -0
- package/scripts/tests/test_models.py +26 -0
- package/scripts/tests/test_package_inventory.py +96 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
- package/scripts/tests/test_policy_lint_selection.py +373 -0
- package/scripts/tests/test_pytest_testpaths.py +66 -0
- package/scripts/tests/test_reporting.py +54 -0
- package/scripts/tests/test_repository_policy.py +71 -0
- package/scripts/tests/test_runner.py +59 -0
- package/scripts/tests/test_tracked_secrets.py +175 -0
- package/scripts/tests/test_tracked_tree.py +28 -0
- package/_shared/advisor/reference/sol-rung.md +0 -39
- package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Flag a prose term that names a code identifier a hair differently.
|
|
2
2
|
|
|
3
3
|
A change adds the field ``premium_request_interactions`` while the same
|
|
4
|
-
branch's docs call it the ``premium-request-budget`` field
|
|
4
|
+
branch's docs call it the ``premium-request-budget`` field. One thing, two
|
|
5
5
|
names, and a reader who searches one never finds the other.
|
|
6
6
|
|
|
7
7
|
::
|
|
@@ -14,38 +14,29 @@ names, and a reader who searches one never finds the other.
|
|
|
14
14
|
|
|
15
15
|
The sweep reads a unified diff, collects the multi-word identifiers added on
|
|
16
16
|
code lines, and scans each added prose line for a hyphen or space variant that
|
|
17
|
-
renames only the final word of an identifier
|
|
18
|
-
and differs on the last. A term that diverges in any earlier
|
|
19
|
-
only one leading word, is ordinary prose. It also spares
|
|
20
|
-
tails, singular/plural forms, stopword windows, and words
|
|
21
|
-
tokens of another introduced identifier.
|
|
17
|
+
renames only the final word of an identifier. The variant agrees on every
|
|
18
|
+
earlier word and differs on the last. A term that diverges in any earlier
|
|
19
|
+
word, or shares only one leading word, is ordinary prose. It also spares
|
|
20
|
+
English compound tails, singular/plural forms, stopword windows, and words
|
|
21
|
+
that are themselves tokens of another introduced identifier.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
import argparse
|
|
25
25
|
import os
|
|
26
26
|
import subprocess
|
|
27
27
|
import sys
|
|
28
|
+
from collections.abc import Sequence
|
|
28
29
|
from pathlib import Path
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
if parent_directory not in sys.path:
|
|
32
|
-
sys.path.insert(0, parent_directory)
|
|
33
|
-
|
|
34
|
-
from pr_loop_shared_constants.terminology_sweep_constants import ( # noqa: E402
|
|
31
|
+
from pr_loop_shared_constants.terminology_sweep_constants import (
|
|
35
32
|
ALL_COMMON_ENGLISH_COMPOUND_TAIL_WORDS,
|
|
33
|
+
ALL_DIFF_FILE_PATH_STRIP_PREFIXES,
|
|
34
|
+
ALL_GIT_DIFF_CACHED_UNIFIED_ZERO_COMMAND,
|
|
36
35
|
ALL_GIT_GREP_BASE_TREE_COMMAND_PREFIX,
|
|
37
36
|
ALL_PROSE_STOPWORD_TOKENS,
|
|
38
37
|
ALL_STRING_ESCAPE_SEQUENCES,
|
|
39
|
-
ALL_TEST_FILE_NAME_INFIX_MARKERS,
|
|
40
|
-
GIT_BASE_TREE_REVISION,
|
|
41
|
-
IDENTIFIER_TOKEN_SEPARATOR,
|
|
42
|
-
PROSE_WINDOW_WORD_SEPARATOR,
|
|
43
|
-
TEST_DIRECTORY_PATH_SEGMENT,
|
|
44
|
-
TEST_FILE_PREFIX,
|
|
45
|
-
TEST_FILE_SUFFIX,
|
|
46
|
-
ALL_DIFF_FILE_PATH_STRIP_PREFIXES,
|
|
47
|
-
ALL_GIT_DIFF_CACHED_UNIFIED_ZERO_COMMAND,
|
|
48
38
|
ALL_SWEEP_CODE_FILE_EXTENSIONS,
|
|
39
|
+
ALL_TEST_FILE_NAME_INFIX_MARKERS,
|
|
49
40
|
CAMEL_CASE_IDENTIFIER_PATTERN,
|
|
50
41
|
CAMEL_CASE_WORD_PATTERN,
|
|
51
42
|
DIFF_ADDED_LINE_PREFIX,
|
|
@@ -55,19 +46,32 @@ from pr_loop_shared_constants.terminology_sweep_constants import ( # noqa: E402
|
|
|
55
46
|
DIFF_NEW_FILE_HEADER_PREFIX,
|
|
56
47
|
DIFF_OLD_FILE_HEADER_PREFIX,
|
|
57
48
|
DIFF_REMOVED_LINE_PREFIX,
|
|
49
|
+
GIT_BASE_TREE_REVISION,
|
|
50
|
+
GIT_DECODE_ERRORS,
|
|
51
|
+
GIT_DIFF_OPERATION,
|
|
58
52
|
GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS,
|
|
53
|
+
GIT_FAILURE_WITHOUT_DETAILS,
|
|
54
|
+
GIT_GREP_OPERATION_TEMPLATE,
|
|
55
|
+
GIT_NO_MATCH_EXIT_CODE,
|
|
56
|
+
GIT_OPERATION_FAILURE_TEMPLATE,
|
|
57
|
+
GIT_TEXT_ENCODING,
|
|
59
58
|
HYPHENATED_PROSE_TOKEN_PATTERN,
|
|
59
|
+
IDENTIFIER_TOKEN_SEPARATOR,
|
|
60
60
|
INLINE_CODE_SPAN_PATTERN,
|
|
61
61
|
JAVASCRIPT_LINE_COMMENT_MARKER,
|
|
62
62
|
JSDOC_CONTINUATION_MARKER,
|
|
63
63
|
MARKDOWN_FILE_EXTENSION,
|
|
64
64
|
MINIMUM_IDENTIFIER_TOKEN_COUNT,
|
|
65
|
+
PROSE_WINDOW_WORD_SEPARATOR,
|
|
65
66
|
PROSE_WORD_PATTERN,
|
|
66
67
|
PYTHON_COMMENT_MARKER,
|
|
67
68
|
SNAKE_CASE_IDENTIFIER_PATTERN,
|
|
68
69
|
STRING_LITERAL_CONTENT_PATTERN,
|
|
69
70
|
TERMINOLOGY_FINDING_TEMPLATE,
|
|
70
71
|
TERMINOLOGY_SWEEP_DESCRIPTION,
|
|
72
|
+
TEST_DIRECTORY_PATH_SEGMENT,
|
|
73
|
+
TEST_FILE_PREFIX,
|
|
74
|
+
TEST_FILE_SUFFIX,
|
|
71
75
|
)
|
|
72
76
|
|
|
73
77
|
IdentifierTuple = tuple[str, ...]
|
|
@@ -173,8 +177,8 @@ def _prose_fragments(file_path: str, line_text: str) -> list[str]:
|
|
|
173
177
|
A Markdown line is prose with its inline-code spans removed, since a
|
|
174
178
|
backticked span names code verbatim. A code line contributes its comment
|
|
175
179
|
tail, its JSDoc continuation text, and the contents of its string
|
|
176
|
-
literals. A test module contributes its comment tail and JSDoc text only
|
|
177
|
-
|
|
180
|
+
literals. A test module contributes its comment tail and JSDoc text only.
|
|
181
|
+
Its string literals hold fixture data, not prose.
|
|
178
182
|
|
|
179
183
|
Args:
|
|
180
184
|
file_path: The path the added line belongs to.
|
|
@@ -201,9 +205,9 @@ def _prose_fragments(file_path: str, line_text: str) -> list[str]:
|
|
|
201
205
|
def _is_test_file(file_path: str) -> bool:
|
|
202
206
|
"""Return whether a diff path names a test code module.
|
|
203
207
|
|
|
204
|
-
A test module's string literals hold fixture data
|
|
205
|
-
generated source, file trees
|
|
206
|
-
reads only its comments and JSDoc lines. A ``quota.test.mjs``, a
|
|
208
|
+
A test module's string literals hold fixture data: embedded diffs,
|
|
209
|
+
generated source, and file trees. That is not documentation prose, so the
|
|
210
|
+
sweep reads only its comments and JSDoc lines. A ``quota.test.mjs``, a
|
|
207
211
|
``layout.spec.ts``, and a ``fixtures.py`` under a tests directory are all
|
|
208
212
|
test modules, so every one of these paths counts::
|
|
209
213
|
|
|
@@ -260,8 +264,8 @@ def _string_literal_fragments(line_text: str) -> list[str]:
|
|
|
260
264
|
Escape sequences inside a literal (``\\n``, ``\\t``) are replaced with
|
|
261
265
|
spaces before the words are read, so an escape letter never glues onto a
|
|
262
266
|
neighbouring word as a phantom prose term. A literal left with no
|
|
263
|
-
whitespace after that replacement is an identifier-shaped value
|
|
264
|
-
a key, a path
|
|
267
|
+
whitespace after that replacement is an identifier-shaped value: a UID,
|
|
268
|
+
a key, or a path. It is not prose, and contributes nothing.
|
|
265
269
|
"""
|
|
266
270
|
all_fragments: list[str] = []
|
|
267
271
|
for each_match in STRING_LITERAL_CONTENT_PATTERN.finditer(line_text):
|
|
@@ -413,9 +417,7 @@ def _word_is_identifier_vocabulary(
|
|
|
413
417
|
return True
|
|
414
418
|
if prose_word.endswith("es") and prose_word[:-2] in all_identifier_tokens:
|
|
415
419
|
return True
|
|
416
|
-
|
|
417
|
-
return True
|
|
418
|
-
return False
|
|
420
|
+
return prose_word.endswith("ies") and prose_word[:-3] + "y" in all_identifier_tokens
|
|
419
421
|
|
|
420
422
|
|
|
421
423
|
def _near_miss_identifier(
|
|
@@ -525,22 +527,8 @@ def _findings_for_line(
|
|
|
525
527
|
return all_findings
|
|
526
528
|
|
|
527
529
|
|
|
528
|
-
def
|
|
529
|
-
diff_text: str,
|
|
530
|
-
all_preexisting_identifier_tuples: frozenset[IdentifierTuple] = frozenset(),
|
|
531
|
-
) -> list[str]:
|
|
532
|
-
"""Return every terminology near-miss finding for a unified diff.
|
|
533
|
-
|
|
534
|
-
Args:
|
|
535
|
-
diff_text: The unified-diff text to sweep.
|
|
536
|
-
all_preexisting_identifier_tuples: Token tuples of added-line identifiers
|
|
537
|
-
the base tree already names. A pre-existing identifier is not one
|
|
538
|
-
the diff introduces, so no prose is flagged against it. Its
|
|
539
|
-
tokens still count as code vocabulary.
|
|
540
|
-
|
|
541
|
-
Returns:
|
|
542
|
-
One finding string per near-miss term on an added prose line.
|
|
543
|
-
"""
|
|
530
|
+
def _find_terminology_near_misses(
|
|
531
|
+
diff_text: str, all_preexisting_identifier_tuples: frozenset[IdentifierTuple]) -> list[str]:
|
|
544
532
|
all_added_lines = _parse_added_lines(diff_text)
|
|
545
533
|
all_identifier_tuples = _collect_introduced_identifiers(all_added_lines)
|
|
546
534
|
all_identifier_tokens = frozenset(
|
|
@@ -571,6 +559,27 @@ def sweep_diff(
|
|
|
571
559
|
return all_findings
|
|
572
560
|
|
|
573
561
|
|
|
562
|
+
def sweep_diff(
|
|
563
|
+
diff_text: str,
|
|
564
|
+
all_preexisting_identifier_tuples: frozenset[IdentifierTuple] = frozenset(),
|
|
565
|
+
) -> list[str]:
|
|
566
|
+
"""Return every terminology near-miss finding for a unified diff.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
diff_text: The unified-diff text to sweep.
|
|
570
|
+
all_preexisting_identifier_tuples: Token tuples of added-line identifiers
|
|
571
|
+
the base tree already names. A pre-existing identifier is not one
|
|
572
|
+
the diff introduces, so no prose is flagged against it. Its
|
|
573
|
+
tokens still count as code vocabulary.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
One finding string per near-miss term on an added prose line.
|
|
577
|
+
"""
|
|
578
|
+
return _find_terminology_near_misses(
|
|
579
|
+
diff_text, all_preexisting_identifier_tuples
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
|
|
574
583
|
def repository_environment() -> dict[str, str]:
|
|
575
584
|
"""Return the process environment with every GIT_ variable removed.
|
|
576
585
|
|
|
@@ -619,8 +628,8 @@ def _base_tree_names(
|
|
|
619
628
|
|
|
620
629
|
Each name is looked up as a whole word in the base revision's tree. A
|
|
621
630
|
repository with no commits yet, or a lookup that git cannot run, reports
|
|
622
|
-
the name as absent, so the sweep then treats it as newly introduced
|
|
623
|
-
the behaviour the sweep has with no base-tree check at all.
|
|
631
|
+
the name as absent, so the sweep then treats it as newly introduced.
|
|
632
|
+
That is the behaviour the sweep has with no base-tree check at all.
|
|
624
633
|
|
|
625
634
|
Args:
|
|
626
635
|
repository_root: The repository root the lookups run in.
|
|
@@ -652,11 +661,90 @@ def _base_tree_names(
|
|
|
652
661
|
return frozenset(all_present_names)
|
|
653
662
|
|
|
654
663
|
|
|
664
|
+
def _run_strict_git(
|
|
665
|
+
repository_root: Path, all_arguments: Sequence[str]
|
|
666
|
+
) -> subprocess.CompletedProcess[str]:
|
|
667
|
+
try:
|
|
668
|
+
return subprocess.run(
|
|
669
|
+
list(all_arguments),
|
|
670
|
+
cwd=str(repository_root),
|
|
671
|
+
capture_output=True,
|
|
672
|
+
text=True,
|
|
673
|
+
encoding=GIT_TEXT_ENCODING,
|
|
674
|
+
errors=GIT_DECODE_ERRORS,
|
|
675
|
+
timeout=GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS,
|
|
676
|
+
check=False,
|
|
677
|
+
env=repository_environment(),
|
|
678
|
+
)
|
|
679
|
+
except (subprocess.TimeoutExpired, OSError) as e:
|
|
680
|
+
raise RuntimeError(str(e)) from e
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _raise_for_git_failure(
|
|
684
|
+
git_process: subprocess.CompletedProcess[str], operation: str
|
|
685
|
+
) -> None:
|
|
686
|
+
if git_process.returncode == 0:
|
|
687
|
+
return
|
|
688
|
+
details = git_process.stderr.strip() or git_process.stdout.strip()
|
|
689
|
+
raise RuntimeError(
|
|
690
|
+
GIT_OPERATION_FAILURE_TEMPLATE.format(
|
|
691
|
+
operation=operation,
|
|
692
|
+
details=details or GIT_FAILURE_WITHOUT_DETAILS,
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _strict_base_tree_names(
|
|
698
|
+
repository_root: Path, all_names: frozenset[str]
|
|
699
|
+
) -> frozenset[str]:
|
|
700
|
+
all_present_names: set[str] = set()
|
|
701
|
+
for each_name in sorted(all_names):
|
|
702
|
+
all_arguments = (
|
|
703
|
+
*ALL_GIT_GREP_BASE_TREE_COMMAND_PREFIX,
|
|
704
|
+
each_name,
|
|
705
|
+
GIT_BASE_TREE_REVISION,
|
|
706
|
+
)
|
|
707
|
+
grep_process = _run_strict_git(repository_root, all_arguments)
|
|
708
|
+
if grep_process.returncode == 0:
|
|
709
|
+
all_present_names.add(each_name)
|
|
710
|
+
continue
|
|
711
|
+
if grep_process.returncode == GIT_NO_MATCH_EXIT_CODE:
|
|
712
|
+
continue
|
|
713
|
+
operation = GIT_GREP_OPERATION_TEMPLATE.format(identifier=each_name)
|
|
714
|
+
_raise_for_git_failure(grep_process, operation)
|
|
715
|
+
return frozenset(all_present_names)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def strict_staged_terminology_findings(repository_root: Path) -> list[str]:
|
|
719
|
+
"""Return near-miss findings for the staged diff.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
repository_root: The repository root the staged diff is read from.
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
One finding string per near-miss term on a staged prose line.
|
|
726
|
+
|
|
727
|
+
Raises:
|
|
728
|
+
RuntimeError: Git could not read the staged diff or look up an
|
|
729
|
+
identifier in HEAD.
|
|
730
|
+
"""
|
|
731
|
+
diff_process = _run_strict_git(
|
|
732
|
+
repository_root, ALL_GIT_DIFF_CACHED_UNIFIED_ZERO_COMMAND
|
|
733
|
+
)
|
|
734
|
+
_raise_for_git_failure(diff_process, GIT_DIFF_OPERATION)
|
|
735
|
+
all_added_names = _identifier_names_on_added_code_lines(diff_process.stdout)
|
|
736
|
+
preexisting_tuples = frozenset(
|
|
737
|
+
_identifier_token_tuple(each_name)
|
|
738
|
+
for each_name in _strict_base_tree_names(repository_root, all_added_names)
|
|
739
|
+
)
|
|
740
|
+
return _find_terminology_near_misses(diff_process.stdout, preexisting_tuples)
|
|
741
|
+
|
|
742
|
+
|
|
655
743
|
def staged_terminology_findings(repository_root: Path) -> list[str]:
|
|
656
744
|
"""Return terminology near-miss findings for a repository's staged diff.
|
|
657
745
|
|
|
658
746
|
An identifier the base tree already names is not one the staged diff
|
|
659
|
-
introduces, so no prose is flagged against it
|
|
747
|
+
introduces, so no prose is flagged against it. Only genuinely new
|
|
660
748
|
identifiers are swept.
|
|
661
749
|
|
|
662
750
|
Args:
|
|
@@ -684,7 +772,7 @@ def staged_terminology_findings(repository_root: Path) -> list[str]:
|
|
|
684
772
|
_identifier_token_tuple(each_name)
|
|
685
773
|
for each_name in _base_tree_names(repository_root, all_added_names)
|
|
686
774
|
)
|
|
687
|
-
return
|
|
775
|
+
return _find_terminology_near_misses(diff_process.stdout, preexisting_tuples)
|
|
688
776
|
|
|
689
777
|
|
|
690
778
|
def _read_diff_text(diff_file_path: str | None) -> str:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import subprocess
|
|
5
|
+
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from types import ModuleType
|
|
7
8
|
|
|
@@ -10,17 +11,26 @@ import pytest
|
|
|
10
11
|
|
|
11
12
|
def _load_sweep_module() -> ModuleType:
|
|
12
13
|
module_path = Path(__file__).parent.parent / "terminology_sweep.py"
|
|
14
|
+
module_directory = str(module_path.parent)
|
|
15
|
+
should_remove_module_directory = module_directory not in sys.path
|
|
16
|
+
if should_remove_module_directory:
|
|
17
|
+
sys.path.insert(0, module_directory)
|
|
13
18
|
spec = importlib.util.spec_from_file_location("terminology_sweep", module_path)
|
|
14
19
|
assert spec is not None
|
|
15
20
|
assert spec.loader is not None
|
|
16
21
|
module = importlib.util.module_from_spec(spec)
|
|
17
|
-
|
|
22
|
+
try:
|
|
23
|
+
spec.loader.exec_module(module)
|
|
24
|
+
finally:
|
|
25
|
+
if should_remove_module_directory:
|
|
26
|
+
sys.path.remove(module_directory)
|
|
18
27
|
return module
|
|
19
28
|
|
|
20
29
|
|
|
21
30
|
sweep_module = _load_sweep_module()
|
|
22
31
|
sweep_diff = sweep_module.sweep_diff
|
|
23
32
|
staged_terminology_findings = sweep_module.staged_terminology_findings
|
|
33
|
+
strict_staged_terminology_findings = sweep_module.strict_staged_terminology_findings
|
|
24
34
|
main = sweep_module.main
|
|
25
35
|
parse_added_lines = sweep_module._parse_added_lines
|
|
26
36
|
|
|
@@ -282,6 +292,8 @@ def test_staged_terminology_findings_flags_staged_prose(tmp_path: Path) -> None:
|
|
|
282
292
|
env=_hermetic_git_environment(),
|
|
283
293
|
)
|
|
284
294
|
findings = staged_terminology_findings(tmp_path)
|
|
295
|
+
with pytest.raises(RuntimeError, match="git grep"):
|
|
296
|
+
strict_staged_terminology_findings(tmp_path)
|
|
285
297
|
assert any("premium-request" in each_finding for each_finding in findings)
|
|
286
298
|
|
|
287
299
|
|
package/bin/cde.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { constants as osConstants } from "node:os";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const lintScriptPath = path.join(packageRoot, "scripts", "cde_lint.py");
|
|
9
|
+
const posixSignalStatusOffset = 128;
|
|
10
|
+
const lintCommandName = "lint";
|
|
11
|
+
const pythonOptionName = "--python";
|
|
12
|
+
const pythonEnvironmentName = "CDE_PYTHON";
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export function createHelpText() {
|
|
16
|
+
return [
|
|
17
|
+
"Usage: cde lint [options]",
|
|
18
|
+
"",
|
|
19
|
+
"Run the policy linter. Source modes are mutually exclusive:",
|
|
20
|
+
" --files PATH [PATH ...] Current worktree files",
|
|
21
|
+
" --staged Staged index content",
|
|
22
|
+
" --base REVISION Changes from a base revision",
|
|
23
|
+
" --repository Tracked worktree files",
|
|
24
|
+
" --text-as PATH One editor buffer from standard input",
|
|
25
|
+
"",
|
|
26
|
+
"Report formats: --format text|json|editor",
|
|
27
|
+
"",
|
|
28
|
+
"Launcher options:",
|
|
29
|
+
" --python <command> Python interpreter override",
|
|
30
|
+
" Exit 0 clean, 1 diagnostics, 2 invalid input or start failure, 3 failed rule",
|
|
31
|
+
"",
|
|
32
|
+
"Python may also be selected with CDE_PYTHON.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export function pythonInterpreterNames(platformName = process.platform) {
|
|
38
|
+
if (platformName === "win32") return ["py", "python"];
|
|
39
|
+
return ["python3", "python"];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export function findPython(
|
|
44
|
+
commandNames = pythonInterpreterNames(),
|
|
45
|
+
canRun = (commandName) => spawnSync(commandName, ["--version"], { stdio: "ignore", shell: false }).status === 0,
|
|
46
|
+
) {
|
|
47
|
+
for (const eachCommand of commandNames) {
|
|
48
|
+
if (canRun(eachCommand)) return eachCommand;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
export function buildLintCommand(interpreter, forwardedArguments) {
|
|
55
|
+
return {
|
|
56
|
+
executable: interpreter,
|
|
57
|
+
arguments: [lintScriptPath, ...forwardedArguments],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
function takePythonOverride(allArguments) {
|
|
63
|
+
const remainingArguments = [];
|
|
64
|
+
let interpreterOverride;
|
|
65
|
+
for (let index = 0; index < allArguments.length; index += 1) {
|
|
66
|
+
if (allArguments[index] === pythonOptionName) {
|
|
67
|
+
const nextArgument = allArguments[index + 1];
|
|
68
|
+
if (nextArgument === undefined || nextArgument.startsWith("--")) {
|
|
69
|
+
return { error: "--python requires an interpreter command" };
|
|
70
|
+
}
|
|
71
|
+
interpreterOverride = nextArgument;
|
|
72
|
+
index += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
remainingArguments.push(allArguments[index]);
|
|
76
|
+
}
|
|
77
|
+
return { interpreterOverride, remainingArguments, error: undefined };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
function attachSignalForwarding(childProcess, processEmitter) {
|
|
82
|
+
const allSignals = ["SIGINT", "SIGTERM"];
|
|
83
|
+
if (process.platform !== "win32") {
|
|
84
|
+
allSignals.push("SIGHUP");
|
|
85
|
+
}
|
|
86
|
+
const listenerBySignal = [];
|
|
87
|
+
for (const eachSignal of allSignals) {
|
|
88
|
+
const listener = () => {
|
|
89
|
+
try {
|
|
90
|
+
childProcess.kill(eachSignal);
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
processEmitter.on(eachSignal, listener);
|
|
96
|
+
listenerBySignal.push([eachSignal, listener]);
|
|
97
|
+
}
|
|
98
|
+
return () => {
|
|
99
|
+
for (const [eachSignal, listener] of listenerBySignal) {
|
|
100
|
+
processEmitter.off(eachSignal, listener);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
function statusCodeForSignal(signalName) {
|
|
107
|
+
const signalNumber = osConstants.signals?.[signalName];
|
|
108
|
+
if (signalNumber === undefined) return 1;
|
|
109
|
+
return posixSignalStatusOffset + signalNumber;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
export function runLintCommand(command, dependencies = {}) {
|
|
114
|
+
const runChild = dependencies.runChild ?? spawn;
|
|
115
|
+
const processEmitter = dependencies.processEmitter ?? process;
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
let childProcess;
|
|
118
|
+
try {
|
|
119
|
+
childProcess = runChild(command.executable, command.arguments, {
|
|
120
|
+
stdio: "inherit",
|
|
121
|
+
shell: false,
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
processEmitter.stderr.write("Unable to start the policy linter.\n");
|
|
126
|
+
resolve(2);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const detachSignals = attachSignalForwarding(childProcess, processEmitter);
|
|
130
|
+
childProcess.on("error", () => {
|
|
131
|
+
detachSignals();
|
|
132
|
+
processEmitter.stderr.write("Unable to start the policy linter.\n");
|
|
133
|
+
resolve(2);
|
|
134
|
+
});
|
|
135
|
+
childProcess.on("close", (exitCode, signalName) => {
|
|
136
|
+
detachSignals();
|
|
137
|
+
if (exitCode !== null) {
|
|
138
|
+
resolve(exitCode);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
resolve(statusCodeForSignal(signalName));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
export async function main(argumentsList = process.argv.slice(2), dependencies = {}) {
|
|
148
|
+
if (argumentsList.length === 0 || argumentsList.includes("--help") || argumentsList.includes("-h")) {
|
|
149
|
+
process.stdout.write(`${createHelpText()}\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (argumentsList[0] !== lintCommandName) {
|
|
153
|
+
process.stderr.write(`${createHelpText()}\n`);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
const { interpreterOverride, remainingArguments, error } = takePythonOverride(argumentsList.slice(1));
|
|
157
|
+
if (error) {
|
|
158
|
+
process.stderr.write(`${error}\n`);
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
const resolvePython = dependencies.findPython ?? findPython;
|
|
162
|
+
const interpreter = interpreterOverride ?? process.env[pythonEnvironmentName] ?? await resolvePython();
|
|
163
|
+
if (!interpreter) {
|
|
164
|
+
process.stderr.write("No usable Python interpreter found. Use --python or CDE_PYTHON.\n");
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
const command = buildLintCommand(interpreter, remainingArguments);
|
|
168
|
+
const runCommand = dependencies.runCommand ?? runLintCommand;
|
|
169
|
+
return await runCommand(command, dependencies);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
const isLaunchedDirectly = process.argv[1] !== undefined
|
|
174
|
+
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
175
|
+
if (isLaunchedDirectly) process.exitCode = await main();
|