Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package skillspector for openSUSE:Factory 
checked in at 2026-09-10 15:37:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/skillspector (Old)
 and      /work/SRC/openSUSE:Factory/.skillspector.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "skillspector"

Thu Sep 10 15:37:32 2026 rev:8 rq:1376832 version:2.11.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/skillspector/skillspector.changes        
2026-09-08 17:07:35.487749726 +0200
+++ /work/SRC/openSUSE:Factory/.skillspector.new.1265/skillspector.changes      
2026-09-10 15:37:35.846474732 +0200
@@ -1,0 +2,20 @@
+Thu Sep 10 05:43:00 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to version 2.11.2:
+  * Deduplicate reference-coverage records so duplicate Markdown
+    references no longer abort with fatal unaccounted_work errors
+  * Account distinct reference targets sharing one source line
+    without conflicting completion records
+  * Stop false static-parser limits on simple runtime parameters,
+    inline skill invocations, PowerShell member access and long
+    quoted prose
+  * Keep runtime-selected printf/command/builtin/env paths marked
+    partially inspected instead of claiming exact reconstruction
+- Deselect two more aarch64-speed tests, both green in upstream CI:
+  test_cross_window_separator_pair_across_public_surfaces (only the
+  is_complete projection fails at the 30 s per-artifact budget; every
+  detection assertion passes) and
+  test_pe3_repeated_nouns_have_bounded_qualifier_cost (1.28 s against
+  a 1.0 s wall-clock bound)
+
+-------------------------------------------------------------------

Old:
----
  skillspector-2.11.1.tar.gz

New:
----
  skillspector-2.11.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ skillspector.spec ++++++
--- /var/tmp/diff_new_pack.Mh6Jeo/_old  2026-09-10 15:37:36.653508357 +0200
+++ /var/tmp/diff_new_pack.Mh6Jeo/_new  2026-09-10 15:37:36.656508482 +0200
@@ -21,7 +21,7 @@
 # %%{primary_python} so it stays correct as the primary interpreter moves.
 %define pythons %{primary_python}
 Name:           skillspector
-Version:        2.11.1
+Version:        2.11.2
 Release:        0
 Summary:        Security scanner for AI agent skills
 License:        Apache-2.0
@@ -164,11 +164,20 @@
 #     2.1 MiB against its own 64 MiB ceiling); only "completed" fails.
 #   * test_rd04_... 31.55 s and test_nine_case_... 31.70 s (~1.26 MB fixture)
 #     -- the same truncation.
-# Note test_cross_window_separator_pair_across_public_surfaces (~512 KB, 84 s)
-# is NOT deselected: it asserts no completeness bound and passes here.
-# test_dense_directory_discovery_... is no longer deselected either -- 2.11.0's
-# O_PATH ancestor traversal took it from 19.73 s to 1.68 s against its own
-# 5.0 s ceiling.
+# Note test_dense_directory_discovery_... is no longer deselected either --
+# 2.11.0's O_PATH ancestor traversal took it from 19.73 s to 1.68 s against
+# its own 5.0 s ceiling.
+#
+# 2.11.2 adds two more aarch64-speed failures of the same fail-closed family
+# (upstream's own test-ci is green: 4013 passed, 0 failed on x86_64):
+#   * test_cross_window_separator_pair_across_public_surfaces is byte-identical
+#     to 2.11.1 and every detection assertion in it still passes here (P1/P9
+#     found, severities/confidences/scores equal) -- only the is_complete
+#     projection fails, with the test taking ~35 s in this chroot at the
+#     same 30 s per-artifact budget. Truncation, not missed detection.
+#   * test_pe3_repeated_nouns_have_bounded_qualifier_cost asserts a 5000-noun
+#     scan finishes in under 1.0 s; aarch64 needs 1.28 s. Pure wall-clock
+#     bound with no detection content (its zero-PE3-finding assertion holds).
 #
 # Deselecting keeps the build gate meaningful without hiding a detection
 # failure: the same resource bounds are asserted deterministically by the
@@ -176,7 +185,7 @@
 # test_static_runtime_limit_is_reported_as_partial,
 # test_static_output_limit_is_reported_as_partial, and the build_context
 # deadline tests), and those do run.
-%pytest --deselect 
tests/unit/test_mcp_server.py::test_mcp_stdio_initialize_registers_scan_skill 
--deselect 
tests/unit/test_input_handler_ssrf.py::TestGitCloneSSRF::test_github_url_allowed
 --deselect 
tests/unit/test_input_handler_ssrf.py::TestGitCloneSSRF::test_gitlab_url_allowed
 --deselect 
tests/unit/test_input_handler_ssrf.py::TestDownloadSSRF::test_raw_githubusercontent_allowed
 --deselect 
tests/unit/test_input_handler_ssrf.py::TestDownloadSSRF::test_download_does_not_follow_redirects
 --deselect 
tests/unit/test_input_handler.py::test_validate_url_host_scp_extracts_github 
--deselect tests/unit/test_input_handler.py::test_scp_valid_host_clones 
--deselect tests/unit/test_input_handler.py::test_https_url_unchanged 
--deselect 
"tests/nodes/analyzers/test_bundled_execution_surface.py::test_discovery_parser_bounds_and_ledger_table[malformed_schema]"
 --deselect 
tests/test_bundled_execution_surface_acceptance.py::test_public_cli_exit_contract
 --deselect tests/test_bundled_execution_surface_
 
acceptance.py::test_recursive_single_child_routes_execution_surfaces_from_child_root
 --deselect 
tests/test_bundled_execution_surface_acceptance.py::test_cli_fail_on_incomplete_is_opt_in
 --deselect 
tests/nodes/test_security_end_to_end.py::test_rd04_large_file_pair_detects_start_boundary_and_end
 --deselect 
tests/nodes/test_security_end_to_end.py::test_nine_case_contract_across_public_surfaces
 --deselect 
tests/nodes/test_security_remediation.py::test_five_megabyte_normalized_static_scan_stays_below_memory_ceiling
+%pytest --deselect 
tests/unit/test_mcp_server.py::test_mcp_stdio_initialize_registers_scan_skill 
--deselect 
tests/unit/test_input_handler_ssrf.py::TestGitCloneSSRF::test_github_url_allowed
 --deselect 
tests/unit/test_input_handler_ssrf.py::TestGitCloneSSRF::test_gitlab_url_allowed
 --deselect 
tests/unit/test_input_handler_ssrf.py::TestDownloadSSRF::test_raw_githubusercontent_allowed
 --deselect 
tests/unit/test_input_handler_ssrf.py::TestDownloadSSRF::test_download_does_not_follow_redirects
 --deselect 
tests/unit/test_input_handler.py::test_validate_url_host_scp_extracts_github 
--deselect tests/unit/test_input_handler.py::test_scp_valid_host_clones 
--deselect tests/unit/test_input_handler.py::test_https_url_unchanged 
--deselect 
"tests/nodes/analyzers/test_bundled_execution_surface.py::test_discovery_parser_bounds_and_ledger_table[malformed_schema]"
 --deselect 
tests/test_bundled_execution_surface_acceptance.py::test_public_cli_exit_contract
 --deselect tests/test_bundled_execution_surface_
 
acceptance.py::test_recursive_single_child_routes_execution_surfaces_from_child_root
 --deselect 
tests/test_bundled_execution_surface_acceptance.py::test_cli_fail_on_incomplete_is_opt_in
 --deselect 
tests/nodes/test_security_end_to_end.py::test_rd04_large_file_pair_detects_start_boundary_and_end
 --deselect 
tests/nodes/test_security_end_to_end.py::test_nine_case_contract_across_public_surfaces
 --deselect 
tests/nodes/test_security_remediation.py::test_five_megabyte_normalized_static_scan_stays_below_memory_ceiling
 --deselect 
tests/nodes/test_security_end_to_end.py::test_cross_window_separator_pair_across_public_surfaces
 --deselect 
tests/unit/test_patterns.py::TestPrivilegeEscalation::test_pe3_repeated_nouns_have_bounded_qualifier_cost
 
 %files
 %license LICENSE

++++++ skillspector-2.11.1.tar.gz -> skillspector-2.11.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/skillspector-2.11.1/CHANGELOG.md 
new/skillspector-2.11.2/CHANGELOG.md
--- old/skillspector-2.11.1/CHANGELOG.md        2020-02-02 01:00:00.000000000 
+0100
+++ new/skillspector-2.11.2/CHANGELOG.md        2020-02-02 01:00:00.000000000 
+0100
@@ -1,3 +1,9 @@
+### 2.11.2 (Thursday, September 10, 2026)
+### Features/Bug Fixes
+* fix: prevent duplicate reference coverage from causing fatal scan-accounting 
errors (#507)
+* fix: avoid false shell-parser limits on ordinary documentation (#507)
+* fix: preserve partial coverage for runtime-selected printf and wrapper paths 
(#508)
+---
 ### 2.11.1 (Monday, September 07, 2026)
 ### Features/Bug Fixes
 * fix: parse space-separated allowed-tools strings (fixes #327) (#330)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/skillspector-2.11.1/PKG-INFO 
new/skillspector-2.11.2/PKG-INFO
--- old/skillspector-2.11.1/PKG-INFO    2020-02-02 01:00:00.000000000 +0100
+++ new/skillspector-2.11.2/PKG-INFO    2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: skillspector
-Version: 2.11.1
+Version: 2.11.2
 Summary: SkillSpector: Security scanner for AI agent skills (Claude Code, 
Cursor, and similar). Scans skills for vulnerabilities, malicious patterns, and 
security risks before installation. Supports Git repos, URLs, zips, and local 
directories; runs static pattern checks and optional LLM semantic analysis; 
outputs terminal, JSON, and Markdown reports with risk scoring.
 Project-URL: Homepage, https://github.com/NVIDIA/skillspector
 Project-URL: Documentation, https://github.com/NVIDIA/skillspector#readme
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/skillspector-2.11.1/docs/release/skillspector-2.11.2.md 
new/skillspector-2.11.2/docs/release/skillspector-2.11.2.md
--- old/skillspector-2.11.1/docs/release/skillspector-2.11.2.md 1970-01-01 
01:00:00.000000000 +0100
+++ new/skillspector-2.11.2/docs/release/skillspector-2.11.2.md 2020-02-02 
01:00:00.000000000 +0100
@@ -0,0 +1,63 @@
+# SkillSpector v2.11.2
+
+Released: 2026-09-10
+
+## Summary
+
+SkillSpector 2.11.2 fixes fatal reference-accounting errors and several false 
static-parser limits triggered by ordinary documentation. This patch also 
preserves incomplete-analysis reporting when a runtime-selected executable 
prevents exact command reconstruction.
+
+## Highlights
+
+- Complete reference accounting when Markdown labels and destinations identify 
the same artifact, or when several referenced artifacts appear on one source 
line.
+- Avoid false parser limits for simple runtime parameters, inline skill 
invocations, PowerShell member access, and long quoted prose.
+- Keep runtime-selected `printf` and wrapper paths marked as partially 
inspected.
+
+## Added
+
+- None.
+
+## Changed
+
+- Record reference-coverage completion once per source line.
+
+## Fixed
+
+- Deduplicate reference-coverage records for the same source file, line, and 
target, preventing fatal `unaccounted_work` errors from duplicate Markdown 
references.
+- Account for distinct reference targets on the same source line without 
creating conflicting completion records.
+- Distinguish simple runtime parameters from command substitutions and complex 
parameter expansions in bounded shell reconstruction, including inline 
`$ARGUMENTS` documentation 
([#464](https://github.com/NVIDIA/SkillSpector/issues/464)).
+- Count unquoted characters separately from already-consumed quoted spans so 
long quoted prose does not cause a false command-word span limit.
+- Preserve partial coverage when runtime parameters select a `printf`, 
`command`, `builtin`, or `env` executable path; a recognized basename alone 
cannot establish which executable will run.
+
+## Security
+
+- Fixed security findings.
+
+## Breaking Changes and Migration
+
+- None. No new configuration is required.
+
+## Deprecations
+
+- None.
+
+## Validation
+
+Validated locally with Python 3.12 and uv 0.10.10:
+
+- `uv lock --check` — passed; third-party dependency versions are unchanged.
+- `uv run --no-sync make test-ci` — 4,013 passed, 14 skipped, 38 deselected, 
and 4 expected failures.
+- `uv run --no-sync make lint` and `uv run --no-sync make format-check` — 
passed.
+- Built wheel and source distributions; `twine check` passed for both 
artifacts.
+- `skillspector --version` — reported `SkillSpector v2.11.2`.
+- The GitHub release helper dry run resolved `v2.11.2` and the matching 
versioned release notes.
+- Docker image build and repository smoke tests passed on Linux/arm64, 
including the local safe fixture and public GitHub repository scans.
+- `git diff --check` — passed.
+
+## Known Limitations
+
+- Full LLM analysis and downstream CI behavior require validation in the 
deployment that uses the release.
+
+## References
+
+- [GitHub PR #507](https://github.com/NVIDIA/SkillSpector/pull/507)
+- [GitHub PR #508](https://github.com/NVIDIA/SkillSpector/pull/508)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/skillspector-2.11.1/pyproject.toml 
new/skillspector-2.11.2/pyproject.toml
--- old/skillspector-2.11.1/pyproject.toml      2020-02-02 01:00:00.000000000 
+0100
+++ new/skillspector-2.11.2/pyproject.toml      2020-02-02 01:00:00.000000000 
+0100
@@ -4,7 +4,7 @@
 
 [project]
 name = "skillspector"
-version = "2.11.1"
+version = "2.11.2"
 description = "SkillSpector: Security scanner for AI agent skills (Claude 
Code, Cursor, and similar). Scans skills for vulnerabilities, malicious 
patterns, and security risks before installation. Supports Git repos, URLs, 
zips, and local directories; runs static pattern checks and optional LLM 
semantic analysis; outputs terminal, JSON, and Markdown reports with risk 
scoring."
 readme = "README.md"
 license = "Apache-2.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/skillspector-2.11.1/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
 
new/skillspector-2.11.2/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
--- 
old/skillspector-2.11.1/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
     2020-02-02 01:00:00.000000000 +0100
+++ 
new/skillspector-2.11.2/src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py
     2020-02-02 01:00:00.000000000 +0100
@@ -53,6 +53,8 @@
 _DESTRUCTIVE_COMMAND_BASENAMES = frozenset({"rm", "del", "erase"})
 _QUOTED_GLOB_SENTINEL = "\ue000"
 _DYNAMIC_SHELL_WORD_SENTINEL = "\ue001"
+_RUNTIME_SHELL_PARAMETER_SENTINEL = "\ue002"
+_SIMPLE_BRACED_PARAMETER_RE = 
re.compile(r"\$\{(?:[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*#?$!-])\}")
 _ROOT_GLOB_DOCUMENTATION_LINE_RE = re.compile(
     r"[ \t]*(?:(?:[-*+]|#{1,6})[ \t]+)?"
     r"(?:(?:(?:documentation|note|example)[ \t]*:[ \t]*)"
@@ -702,10 +704,18 @@
         if word is None:
             return False, False
         if _DYNAMIC_SHELL_WORD_SENTINEL in word:
-            # A runtime expansion participates in the invocation or wrapper
-            # command word. Its executable basename is not deterministic.
+            # A command substitution or complex expansion participates in the
+            # invocation or wrapper word. Its basename is not deterministic.
             return True, False
         command = word.casefold().rsplit("/", 1)[-1]
+        if _RUNTIME_SHELL_PARAMETER_SENTINEL in word and command in {
+            "printf",
+            "command",
+            "builtin",
+            "env",
+        }:
+            # A known basename does not make a runtime-selected executable 
exact.
+            return True, False
         if command == "printf":
             return True, True
         if command == "command":
@@ -789,6 +799,17 @@
     return True, arguments
 
 
+def _invocation_expansion_marker(content: str, start: int, end: int) -> str:
+    """Distinguish runtime-only parameters from possible command 
reconstruction."""
+    if content.startswith("$(", start) or (
+        content.startswith("${", start)
+        and _SIMPLE_BRACED_PARAMETER_RE.fullmatch(content, start, end) is None
+    ):
+        # Complex parameter expansions may contain nested command 
substitutions.
+        return _DYNAMIC_SHELL_WORD_SENTINEL
+    return _RUNTIME_SHELL_PARAMETER_SENTINEL
+
+
 def _next_shell_invocation_word(
     content: str,
     start: int,
@@ -820,6 +841,7 @@
     quote: str | None = None
     ansi_c_quote = False
     word_started = False
+    unquoted_characters = 0
     while cursor < limit:
         if cursor % 4096 == 0:
             check_runtime()
@@ -884,7 +906,7 @@
                     word_started = True
                     cursor += 1
                     continue
-                output.append(_DYNAMIC_SHELL_WORD_SENTINEL)
+                output.append(_invocation_expansion_marker(content, cursor, 
parameter_end))
                 word_started = True
                 cursor = parameter_end
                 if inherited_quote_closed[0]:
@@ -959,7 +981,8 @@
                 word_started = True
                 cursor += 1
                 continue
-            output.append(_DYNAMIC_SHELL_WORD_SENTINEL)
+            # A simple runtime parameter does not invoke the printf evaluator.
+            output.append(_invocation_expansion_marker(content, cursor, 
parameter_end))
             word_started = True
             cursor = parameter_end
             continue
@@ -1002,7 +1025,8 @@
         else:
             output.append(character)
             word_started = True
-            if len(output) > _SHELL_COMMAND_WORD_CHARS:
+            unquoted_characters += 1
+            if unquoted_characters > _SHELL_COMMAND_WORD_CHARS:
                 return None, cursor, True
         cursor += 1
     if quote is not None:
@@ -1151,6 +1175,7 @@
     ansi_c_quote = False
     dynamic = False
     limited = False
+    unquoted_characters = 0
     cursor = start
     limit = len(content)
     while cursor < limit:
@@ -1354,7 +1379,11 @@
             break
         else:
             output.append(character)
-            if len(output) > _SHELL_COMMAND_WORD_CHARS:
+            # Quoted spans have already been consumed in full. Their decoded
+            # length must not exhaust the budget for the following literal
+            # suffix, which can resolve the candidate as an ordinary word.
+            unquoted_characters += 1
+            if unquoted_characters > _SHELL_COMMAND_WORD_CHARS:
                 return _ShellCommandWord("".join(output), cursor, dynamic, 
limited=True)
         cursor += 1
     if quote is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/skillspector-2.11.1/src/skillspector/nodes/finalize_inspection_ledger.py 
new/skillspector-2.11.2/src/skillspector/nodes/finalize_inspection_ledger.py
--- 
old/skillspector-2.11.1/src/skillspector/nodes/finalize_inspection_ledger.py    
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/skillspector-2.11.2/src/skillspector/nodes/finalize_inspection_ledger.py    
    2020-02-02 01:00:00.000000000 +0100
@@ -39,6 +39,7 @@
         if outcome in {"partial", "failed", "out_of_scope"}:
             exceptional_outcomes.setdefault(str(event.get("path", "")), 
set()).add(outcome)
     findings: list[Finding] = []
+    seen_locations: set[tuple[str, int, str]] = set()
     for reference in raw_references:
         if not isinstance(reference, dict):
             continue
@@ -62,6 +63,14 @@
         if final_disposition not in {"partial", "failed", "out_of_scope"}:
             continue
         line_value = reference.get("line", 1)
+        source_path = str(reference.get("source_path", "SKILL.md"))
+        line = line_value if isinstance(line_value, int) else 1
+        # A Markdown label and destination may resolve to the same artifact.
+        # Findings identify source lines, so emit that coverage gap only once.
+        location = (source_path, line, target_path)
+        if location in seen_locations:
+            continue
+        seen_locations.add(location)
         evidence = str(reference.get("evidence", ""))[:160]
         findings.append(
             Finding(
@@ -69,8 +78,8 @@
                 message="Referenced artifact was not completely inspected",
                 severity="HIGH",
                 confidence=1.0,
-                file=str(reference.get("source_path", "SKILL.md")),
-                start_line=line_value if isinstance(line_value, int) else 1,
+                file=source_path,
+                start_line=line,
                 category="analysis-evasion",
                 tags=["coverage", "reference", 
f"target-disposition:{final_disposition}"],
                 finding=f"{target_path} ({final_disposition})"[:200],
@@ -88,17 +97,24 @@
 def finalize_inspection_ledger(state: SkillspectorState) -> dict[str, object]:
     """Validate full internal facts and derive the public completeness 
projection."""
     reference_findings = _reference_coverage_findings(state)
+    # Work IDs are scoped to analyzer, source file and line range. Distinct
+    # targets on one line must share a terminal row with all emitted findings.
+    reference_ids_by_line: dict[tuple[str, int | None], list[str]] = {}
+    for finding in reference_findings:
+        reference_ids_by_line.setdefault((finding.file, finding.start_line), 
[]).append(
+            finding.finding_id
+        )
     reference_events: list[InspectionLedgerEvent] = [
         ledger_event(
             outcome=LedgerOutcome.COMPLETED,
             phase="reference",
             analyzer_id="reference_coverage",
-            path=finding.file,
-            start_line=finding.start_line,
-            end_line=finding.start_line,
-            emitted_finding_ids=[finding.finding_id],
+            path=path,
+            start_line=line,
+            end_line=line,
+            emitted_finding_ids=finding_ids,
         )
-        for finding in reference_findings
+        for (path, line), finding_ids in reference_ids_by_line.items()
     ]
     merged_state = dict(state)
     all_findings = [*(state.get("findings") or []), *reference_findings]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/skillspector-2.11.1/tests/nodes/analyzers/test_security_reconstruction.py 
new/skillspector-2.11.2/tests/nodes/analyzers/test_security_reconstruction.py
--- 
old/skillspector-2.11.1/tests/nodes/analyzers/test_security_reconstruction.py   
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/skillspector-2.11.2/tests/nodes/analyzers/test_security_reconstruction.py   
    2020-02-02 01:00:00.000000000 +0100
@@ -1619,6 +1619,81 @@
 
 
 @pytest.mark.parametrize(
+    "content",
+    [
+        "Interpret `$ARGUMENTS` as the user's input.",
+        "Interpret `${ARGUMENTS}` as the user's input.",
+        "Use `$example:task FILE_PATH|--all` to invoke the skill.",
+        "Read the token from `$SERVICE_TOKEN` or a configured file.",
+        'Test-Path "$($_.FullName)\\cli-path"',
+        r"Render `$$\int_0^1 x \, dx$$` as math.",
+        "The reader's " + "ordinary documentation\n" * 220 + " author's 
guide.\n",
+        '"""Reader documentation.\n' + "ordinary documentation\n" * 220 + 
'"""\nreturn None\n',
+    ],
+    ids=[
+        "parameter",
+        "braced-parameter",
+        "skill-invocation",
+        "token",
+        "powershell",
+        "math",
+        "apostrophe",
+        "docstring",
+    ],
+)
+def test_documentation_is_not_a_bounded_shell_reconstruction(content: str) -> 
None:
+    result = static_runner.run_static_patterns_with_ledger(
+        {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}, 
[tm_module]
+    )
+
+    assert result["inspection_ledger"][0]["outcome"] is LedgerOutcome.COMPLETED
+    assert not any(finding.rule_id == "TM1" for finding in result["findings"])
+
+
+def test_long_quoted_command_path_still_detects_destructive_basename() -> None:
+    content = 'r"' + "directory/" * 500 + '"rm -rf /'
+    result = static_runner.run_static_patterns_with_ledger(
+        {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}, 
[tm_module]
+    )
+
+    assert any(finding.rule_id == "TM1" for finding in result["findings"])
+
+
[email protected](
+    "content",
+    [
+        "$(printf $FORMAT) -rf /",
+        "$(printf ${FORMAT}) -rf /",
+        "$($BIN/printf echo) -rf /",
+        "$(${BIN}/printf echo) -rf /",
+        '$("$BIN/printf" echo) -rf /',
+        "$($BIN/env printf echo) -rf /",
+        "$($BIN/command printf echo) -rf /",
+        "$($BIN/builtin printf echo) -rf /",
+        '`"${TOOL:-$(printf rm ' + " " * 300 + ')}"` -rf /',
+    ],
+    ids=[
+        "runtime-format",
+        "braced-runtime-format",
+        "runtime-path",
+        "braced-runtime-path",
+        "quoted-runtime-path",
+        "runtime-env-path",
+        "runtime-command-path",
+        "runtime-builtin-path",
+        "nested-parameter-reconstruction",
+    ],
+)
+def 
test_runtime_printf_arguments_and_nested_reconstruction_stay_partial(content: 
str) -> None:
+    result = static_runner.run_static_patterns_with_ledger(
+        {"components": ["SKILL.md"], "file_cache": {"SKILL.md": content}}, 
[tm_module]
+    )
+
+    assert result["inspection_ledger"][0]["outcome"] is LedgerOutcome.PARTIAL
+    assert result["inspection_ledger"][0]["reason_code"] is 
LedgerReason.STATIC_PARSE_LIMIT
+
+
[email protected](
     "printf_command",
     ["printf", 'p"rintf"', "p'rintf'", '"pri"ntf', r"p\rintf", "env printf"],
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/skillspector-2.11.1/tests/nodes/test_finalize_inspection_ledger.py 
new/skillspector-2.11.2/tests/nodes/test_finalize_inspection_ledger.py
--- old/skillspector-2.11.1/tests/nodes/test_finalize_inspection_ledger.py      
2020-02-02 01:00:00.000000000 +0100
+++ new/skillspector-2.11.2/tests/nodes/test_finalize_inspection_ledger.py      
2020-02-02 01:00:00.000000000 +0100
@@ -610,6 +610,76 @@
     assert completeness["is_complete"] is False
 
 
[email protected]("failed", [False, True])
[email protected](
+    "locations",
+    [
+        [("a.md", 4, 8), ("a.md", 4, 24)],
+        [("a.md", 4, 8), ("b.md", 4, 24)],
+        [("a.md", 4, 8), ("a.md", 7, 8)],
+        [("a.md", 4, 8), ("a.md", 4, 24), ("b.md", 4, 40), ("a.md", 7, 8)],
+    ],
+)
+def test_reference_findings_share_one_terminal_event_per_source_line(
+    locations: list[tuple[str, int, int]], failed: bool
+) -> None:
+    paths = sorted({path for path, _, _ in locations})
+    result = finalize_inspection_ledger(
+        {
+            "components": ["SKILL.md", *paths],
+            "findings": [],
+            "effective_finding_ids": [],
+            "artifact_inventory": [
+                {"path": path, "disposition": "analyzed", "content_kind": 
"text"} for path in paths
+            ],
+            "artifact_references": [
+                {
+                    "source_path": "SKILL.md",
+                    "line": line,
+                    "column": column,
+                    "evidence": f"Read [{path}]({path}).",
+                    "target_path": path,
+                    "status": "resolved",
+                    "disposition": "analyzed",
+                }
+                for path, line, column in locations
+            ],
+            "inspection_ledger": [
+                ledger_event(
+                    outcome=LedgerOutcome.FAILED if failed else 
LedgerOutcome.PARTIAL,
+                    record_type=LedgerRecordType.SYSTEM,
+                    phase="static",
+                    path=path,
+                    reason=LedgerReason.READ_ERROR if failed else 
LedgerReason.STATIC_PARSE_LIMIT,
+                )
+                for path in paths
+            ],
+            "analyzer_status_events": [],
+        }
+    )
+
+    findings = result["findings"]
+    expected_locations = {(line, path) for path, line, _ in locations}
+    assert len(findings) == len(expected_locations)
+    assert {
+        (finding.start_line, finding.matched_text) for finding in findings
+    } == expected_locations
+    events = result["inspection_ledger"]
+    assert len(events) == len({line for _, line, _ in locations})
+    for event in events:
+        assert set(event["emitted_finding_ids"]) == {
+            finding.finding_id for finding in findings if finding.start_line 
== event["start_line"]
+        }
+    completeness = result["analysis_completeness"]
+    assert completeness["status"] == ("failed" if failed else "partial")
+    assert result["execution_successful"] is (not failed)
+    assert len(result["effective_finding_ids"]) == len(expected_locations)
+    assert not any(
+        row["reason_code"] in {"unaccounted_work", "finding_accounting_error"}
+        for row in completeness["ledger_exceptions"]
+    )
+
+
 @pytest.mark.parametrize("use_llm", [False, True])
 @pytest.mark.parametrize(
     ("disposition", "outcome", "reason", "expected_ae1"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/skillspector-2.11.1/tests/nodes/test_security_end_to_end.py 
new/skillspector-2.11.2/tests/nodes/test_security_end_to_end.py
--- old/skillspector-2.11.1/tests/nodes/test_security_end_to_end.py     
2020-02-02 01:00:00.000000000 +0100
+++ new/skillspector-2.11.2/tests/nodes/test_security_end_to_end.py     
2020-02-02 01:00:00.000000000 +0100
@@ -1005,6 +1005,59 @@
     await _assert_incomplete_across_public_surfaces(tmp_path, result)
 
 
+def 
test_markdown_reference_to_parser_limited_target_keeps_cli_execution_successful(
+    tmp_path: Path,
+) -> None:
+    _write_bundle(
+        tmp_path,
+        {
+            "SKILL.md": (
+                "---\nname: reference-coverage\ndescription: Reference 
coverage regression\n---\n"
+                "Read [references/commands.md](references/commands.md).\n"
+            ),
+            "references/commands.md": "$(env env env env printf rm) -rf /\n",
+        },
+    )
+
+    report = _scan_cli(tmp_path)
+
+    assert report["execution_successful"] is True
+    completeness = report["analysis_completeness"]
+    assert completeness["status"] == "partial"
+    assert completeness["is_complete"] is False
+    assert any(
+        row["reason_code"] == "static_parse_limit" for row in 
completeness["ledger_exceptions"]
+    )
+    assert not any(row["fatal"] for row in completeness["ledger_exceptions"])
+    ae1 = [issue for issue in report["issues"] if issue["id"] == "AE1"]
+    assert len(ae1) == 1
+    assert ae1[0]["location"]["file"] == "SKILL.md"
+    assert ae1[0]["location"]["start_line"] == 5
+
+
+def 
test_referenced_variable_documentation_does_not_create_coverage_gaps(tmp_path: 
Path) -> None:
+    _write_bundle(
+        tmp_path,
+        {
+            "SKILL.md": (
+                "---\nname: reference-variables\ndescription: Variable 
documentation\n---\n"
+                "Read [references/usage.md](references/usage.md).\n"
+            ),
+            "references/usage.md": (
+                "Interpret `$ARGUMENTS` as the requested input.\n"
+                'In PowerShell, use `Test-Path "$($_.FullName)\\cli-path"`.\n'
+            ),
+        },
+    )
+
+    report = _scan_cli(tmp_path)
+
+    assert report["execution_successful"] is True
+    assert report["analysis_completeness"]["status"] == "complete"
+    assert report["analysis_completeness"]["ledger_exceptions"] == []
+    assert not any(issue["id"] == "AE1" for issue in report["issues"])
+
+
 @pytest.mark.asyncio
 async def test_changed_rule_family_negative_controls(tmp_path: Path) -> None:
     _write_bundle(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/skillspector-2.11.1/uv.lock 
new/skillspector-2.11.2/uv.lock
--- old/skillspector-2.11.1/uv.lock     2020-02-02 01:00:00.000000000 +0100
+++ new/skillspector-2.11.2/uv.lock     2020-02-02 01:00:00.000000000 +0100
@@ -2687,7 +2687,7 @@
 
 [[package]]
 name = "skillspector"
-version = "2.11.1"
+version = "2.11.2"
 source = { editable = "." }
 dependencies = [
     { name = "boto3" },

Reply via email to