cc-session-tools 3.7.0__tar.gz → 3.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cc_session_tools-3.7.0/src/cc_session_tools.egg-info → cc_session_tools-3.8.0}/PKG-INFO +20 -1
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/README.md +19 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/pyproject.toml +1 -1
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccst.py +75 -17
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/doctor.py +11 -6
- cc_session_tools-3.8.0/src/cc_session_tools/lib/pdata/readiness.py +319 -0
- cc_session_tools-3.8.0/src/cc_session_tools/lib/text_wrap.py +35 -0
- cc_session_tools-3.8.0/src/cc_session_tools/skills/pm-pdata-do-audit-and-prepare-to-migrate/SKILL.md +162 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0/src/cc_session_tools.egg-info}/PKG-INFO +20 -1
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/SOURCES.txt +6 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_doctor.py +23 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hooks_install_zero_arg.py +47 -11
- cc_session_tools-3.8.0/tests/test_ccst_pdata_readiness_cli.py +95 -0
- cc_session_tools-3.8.0/tests/test_hooks_install_records.py +104 -0
- cc_session_tools-3.8.0/tests/test_pdata_audit_skill.py +89 -0
- cc_session_tools-3.8.0/tests/test_text_wrap.py +70 -0
- cc_session_tools-3.7.0/src/cc_session_tools/skills/pm-pdata-do-audit-and-prepare-to-migrate/SKILL.md +0 -199
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/LICENSE +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/setup.cfg +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/__init__.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/__init__.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccd.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccr.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccs.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/ccsched.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/config/hooks-bundle.json +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/hooks_install.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/__init__.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/bun_tools.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/context_overrides.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/db.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/debug.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/hook_registry.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/install_sync.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/machine_identity.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/occupancy.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/paths.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/dump.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/resolve.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/sync_notify.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/picker.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/proc_lock.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/prompts.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/roots.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/rules.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/digest.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/ledger.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/surface.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/session_gc.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/sessions.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/sessions_db.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/shell_install.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/tasklist.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/clean-hook-sessions/scripts/clean-hook-sessions.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/compact-smart/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/extract-pptx/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/extract-pptx/scripts/extract_pptx.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/extract-to-text/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/extract-to-text/scripts/extract_to_text.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-pdata-design-schema/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-pdata-do-init/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-pdata-do-manual-migration/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-pdata-resolve-conflicts/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/__init__.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/attribution.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/cache.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/data/pricing.json +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/parent_inference.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/parser.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/pricing.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/query.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/report.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/schema.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/claude_code_usage/session_names.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/__init__.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/after_response.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/bash_hard_deny.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/bash_security_review.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/cache.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/catchup.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/confirm_8digit.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/context_window_warning.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/last_screenshot.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/marker_allow.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/markers.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/messaging_deliver.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/model_info.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/normalise.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/pdata_sync.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/pending_migration.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/pending_rename.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/script_allowlist.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/session_tag.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/stats.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/telemetry.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/telemetry_query.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/telemetry_trim.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/transcript.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/src/hooks/worklog_guard.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_after_response.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_attribution.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_bash_hard_deny.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_bash_security_review.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_bash_security_review_allowlist.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_bun_tools.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cache.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cache_sqlite.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccr_orphans.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_emptiness.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_eta.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_list_mode.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_scope_flags.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_sentinel_sort.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccs_session_counts.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_bundle_discovery.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_context_override_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_gc_prune.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_gc_report.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hook_dispatcher.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hooks_allowlist.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hooks_install_prune.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_hooks_uninstall.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_install_everything.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_machine_identity_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_main_gate.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_init_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_sync_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_pdata_verify_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_repair_sessions_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_sessions_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_shell_install.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_skills_install.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_skills_uninstall.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_telemetry_query.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccst_telemetry_trim.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_ccusage_wrapper.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_claude_flags.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_claude_md_install.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_ccd.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_ccd_dry_run.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_ccr.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_ccs.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_cli_version.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_confirm_8digit.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_context_overrides.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_context_window_warning.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_db.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_debug.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_doctor_mutes.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_empty_session.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_hook_cache.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_hooks_install.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_install_sync.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_last_screenshot.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_levenshtein.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_machine_identity.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_marker_allow.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_migrate_ccmsg.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_migrate_ccsched.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_migrate_sessions_db.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_migrate_telemetry.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_model_info.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_normalise.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_occupancy.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_parent_inference.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_parser.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_paths.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_pending_migration_hook.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_picker.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_pricing.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_proc_lock.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_prompts.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_query.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_report.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_roots.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_rules.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_scheduler_bundled_jobs.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_schema.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_script_allowlist.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_session_names.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_session_tag.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_sessions.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_sessions_db.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_sessions_repair.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_tasklist.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_telemetry.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_telemetry_store.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_transcript.py +0 -0
- {cc_session_tools-3.7.0 → cc_session_tools-3.8.0}/tests/test_worklog_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cc-session-tools
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.0
|
|
4
4
|
Summary: Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (hooks package)
|
|
5
5
|
Author: raffishquartan
|
|
6
6
|
License: MIT License
|
|
@@ -928,6 +928,25 @@ ccst context-override off # re-enable the warnings
|
|
|
928
928
|
ccst context-override status # report the current state
|
|
929
929
|
```
|
|
930
930
|
|
|
931
|
+
### `ccst pdata readiness-scan`
|
|
932
|
+
|
|
933
|
+
A read-only scan of a project's CSV files for the mechanical blockers to a `ccst pdata init`
|
|
934
|
+
migration, used by the `pm-pdata-do-audit-and-prepare-to-migrate` skill and useful on its own:
|
|
935
|
+
|
|
936
|
+
```sh
|
|
937
|
+
ccst pdata readiness-scan --project myproject # compact Markdown report
|
|
938
|
+
ccst pdata readiness-scan --project myproject --format json # machine-readable
|
|
939
|
+
ccst pdata readiness-scan --project myproject --path notes/ --findings-only # one folder's findings
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
It reports ragged rows, empty/duplicate headers, mixed date formats, mixed `|`/`;` separators,
|
|
943
|
+
machine-specific paths, null-like placeholder strings, duplicate rows, comment rows, repeated
|
|
944
|
+
header rows, byte-order marks, CRLF line endings and unreadable files, plus a per-file inventory
|
|
945
|
+
and the columns that are unique on every row. Findings carry counts and row numbers, never cell
|
|
946
|
+
text, and a unique column is a fact rather than a recommended key. It exits 0 whenever the scan
|
|
947
|
+
ran (findings are not failures), and 2 when the project directory does not exist. It scans exactly
|
|
948
|
+
the CSVs `ccst pdata init` would classify.
|
|
949
|
+
|
|
931
950
|
### `ccst skills install`
|
|
932
951
|
|
|
933
952
|
> You normally don't need this: `ccst install-everything --apply` runs it, and `ccst` runs that
|
|
@@ -871,6 +871,25 @@ ccst context-override off # re-enable the warnings
|
|
|
871
871
|
ccst context-override status # report the current state
|
|
872
872
|
```
|
|
873
873
|
|
|
874
|
+
### `ccst pdata readiness-scan`
|
|
875
|
+
|
|
876
|
+
A read-only scan of a project's CSV files for the mechanical blockers to a `ccst pdata init`
|
|
877
|
+
migration, used by the `pm-pdata-do-audit-and-prepare-to-migrate` skill and useful on its own:
|
|
878
|
+
|
|
879
|
+
```sh
|
|
880
|
+
ccst pdata readiness-scan --project myproject # compact Markdown report
|
|
881
|
+
ccst pdata readiness-scan --project myproject --format json # machine-readable
|
|
882
|
+
ccst pdata readiness-scan --project myproject --path notes/ --findings-only # one folder's findings
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
It reports ragged rows, empty/duplicate headers, mixed date formats, mixed `|`/`;` separators,
|
|
886
|
+
machine-specific paths, null-like placeholder strings, duplicate rows, comment rows, repeated
|
|
887
|
+
header rows, byte-order marks, CRLF line endings and unreadable files, plus a per-file inventory
|
|
888
|
+
and the columns that are unique on every row. Findings carry counts and row numbers, never cell
|
|
889
|
+
text, and a unique column is a fact rather than a recommended key. It exits 0 whenever the scan
|
|
890
|
+
ran (findings are not failures), and 2 when the project directory does not exist. It scans exactly
|
|
891
|
+
the CSVs `ccst pdata init` would classify.
|
|
892
|
+
|
|
874
893
|
### `ccst skills install`
|
|
875
894
|
|
|
876
895
|
> You normally don't need this: `ccst install-everything --apply` runs it, and `ccst` runs that
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cc-session-tools"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.8.0"
|
|
8
8
|
description = "Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (hooks package)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -112,6 +112,7 @@ from cc_session_tools.hooks_install import (
|
|
|
112
112
|
)
|
|
113
113
|
from cc_session_tools.lib import machine_identity
|
|
114
114
|
from cc_session_tools.lib.hook_registry import HOOK_DESCRIPTIONS, HOOK_VERBS
|
|
115
|
+
from cc_session_tools.lib.text_wrap import wrap_block
|
|
115
116
|
|
|
116
117
|
if TYPE_CHECKING:
|
|
117
118
|
# Type-only, so the handler's own lazy `from ... import sync_check` (this file's convention:
|
|
@@ -538,30 +539,45 @@ def _bundle_inventory(bundle: dict[str, Any]) -> list[tuple[str, str, str | None
|
|
|
538
539
|
return out
|
|
539
540
|
|
|
540
541
|
|
|
541
|
-
def
|
|
542
|
+
def _format_hooks_install_records(
|
|
542
543
|
inventory: list[tuple[str, str, str | None, str]],
|
|
543
544
|
added_keys: set[tuple[str, str | None, str]],
|
|
544
|
-
) ->
|
|
545
|
-
"""
|
|
546
|
-
|
|
547
|
-
if not inventory:
|
|
548
|
-
return
|
|
545
|
+
) -> str:
|
|
546
|
+
"""One record per hook: a `name status` header, an indented `Events:` line, then the
|
|
547
|
+
indented, terminal-width-wrapped description. Records are separated by a blank line.
|
|
549
548
|
|
|
550
|
-
|
|
549
|
+
A hook registered for several events is one record; when only some of its registrations
|
|
550
|
+
would be added, those events carry a `(new)` marker. Order follows the inventory (sorted by
|
|
551
|
+
hook name).
|
|
552
|
+
"""
|
|
553
|
+
by_hook: dict[str, list[tuple[bool, str]]] = {}
|
|
551
554
|
for name, event, matcher, cmd in inventory:
|
|
552
|
-
status = "install" if (event, matcher, cmd) in added_keys else "already-installed"
|
|
553
555
|
event_label = f"{event}[{matcher}]" if matcher else event
|
|
556
|
+
by_hook.setdefault(name, []).append(((event, matcher, cmd) in added_keys, event_label))
|
|
557
|
+
|
|
558
|
+
records: list[str] = []
|
|
559
|
+
for name, registrations in by_hook.items():
|
|
560
|
+
any_new = any(is_new for is_new, _ in registrations)
|
|
561
|
+
mixed = any_new and not all(is_new for is_new, _ in registrations)
|
|
562
|
+
status = "install" if any_new else "already-installed"
|
|
563
|
+
events = ", ".join(
|
|
564
|
+
f"{label} (new)" if mixed and is_new else label for is_new, label in registrations
|
|
565
|
+
)
|
|
566
|
+
lines = [f"{name} {status}", wrap_block(f"Events: {events}")]
|
|
554
567
|
description = HOOK_DESCRIPTIONS.get(name, "")
|
|
555
|
-
|
|
568
|
+
if description:
|
|
569
|
+
lines.append(wrap_block(description))
|
|
570
|
+
records.append("\n".join(lines))
|
|
571
|
+
return "\n\n".join(records)
|
|
556
572
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
]
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
print(
|
|
573
|
+
|
|
574
|
+
def _print_hooks_install_table(
|
|
575
|
+
inventory: list[tuple[str, str, str | None, str]],
|
|
576
|
+
added_keys: set[tuple[str, str | None, str]],
|
|
577
|
+
) -> None:
|
|
578
|
+
"""Print the per-hook install records to stdout."""
|
|
579
|
+
if inventory:
|
|
580
|
+
print(_format_hooks_install_records(inventory, added_keys))
|
|
565
581
|
|
|
566
582
|
|
|
567
583
|
def _list_bundle_hook_names(bundle: dict[str, Any]) -> list[str]:
|
|
@@ -1842,6 +1858,28 @@ def _cmd_pdata_resolve(args: argparse.Namespace) -> int:
|
|
|
1842
1858
|
return 1 if outstanding else 0
|
|
1843
1859
|
|
|
1844
1860
|
|
|
1861
|
+
def _cmd_pdata_readiness_scan(args: argparse.Namespace) -> int:
|
|
1862
|
+
from cc_session_tools.lib.pdata import readiness, store
|
|
1863
|
+
|
|
1864
|
+
try:
|
|
1865
|
+
root = store.project_root(args.project)
|
|
1866
|
+
except ValueError as exc:
|
|
1867
|
+
print(f"ccst pdata readiness-scan: {exc}", file=sys.stderr)
|
|
1868
|
+
return 2
|
|
1869
|
+
if not root.is_dir():
|
|
1870
|
+
print(
|
|
1871
|
+
f"ccst pdata readiness-scan: project directory does not exist: {root}",
|
|
1872
|
+
file=sys.stderr,
|
|
1873
|
+
)
|
|
1874
|
+
return 2
|
|
1875
|
+
report = readiness.scan_project(root, path_prefix=args.path)
|
|
1876
|
+
if args.format == "json":
|
|
1877
|
+
print(readiness.format_json(report))
|
|
1878
|
+
else:
|
|
1879
|
+
print(readiness.format_markdown(report, findings_only=args.findings_only))
|
|
1880
|
+
return 0
|
|
1881
|
+
|
|
1882
|
+
|
|
1845
1883
|
def _cmd_pdata_sync_check(args: argparse.Namespace) -> int:
|
|
1846
1884
|
"""The spec's "Hourly `ccsched` job" trigger: per project, rehydrate-check and - only if no
|
|
1847
1885
|
rehydrate happened - dump-check. The whole decision lives in `pdata.sync_check` (shared with
|
|
@@ -3240,6 +3278,24 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
3240
3278
|
"the default one-line summary (--project always prints full detail)",
|
|
3241
3279
|
)
|
|
3242
3280
|
|
|
3281
|
+
pdata_readiness_parser = pdata_sub.add_parser(
|
|
3282
|
+
"readiness-scan",
|
|
3283
|
+
help="Scan a project's CSVs for pdata-migration blockers (read-only)",
|
|
3284
|
+
)
|
|
3285
|
+
pdata_readiness_parser.add_argument("--project", metavar="NAME", required=True)
|
|
3286
|
+
pdata_readiness_parser.add_argument(
|
|
3287
|
+
"--format", choices=("markdown", "json"), default="markdown",
|
|
3288
|
+
help="Output format (default: markdown)",
|
|
3289
|
+
)
|
|
3290
|
+
pdata_readiness_parser.add_argument(
|
|
3291
|
+
"--path", metavar="PREFIX", default=None,
|
|
3292
|
+
help="Only scan CSVs whose project-relative path starts with PREFIX",
|
|
3293
|
+
)
|
|
3294
|
+
pdata_readiness_parser.add_argument(
|
|
3295
|
+
"--findings-only", action="store_true",
|
|
3296
|
+
help="Omit the per-file inventory from the Markdown report",
|
|
3297
|
+
)
|
|
3298
|
+
|
|
3243
3299
|
pdata_dump_parser = pdata_sub.add_parser(
|
|
3244
3300
|
"dump", help="Publish the current local DB state to .pdata-db-dump/latest.sql"
|
|
3245
3301
|
)
|
|
@@ -3643,6 +3699,8 @@ def main() -> None:
|
|
|
3643
3699
|
sys.exit(_cmd_pdata_reconcile_session_output(args))
|
|
3644
3700
|
if args.verb == "verify":
|
|
3645
3701
|
sys.exit(_cmd_pdata_verify(args))
|
|
3702
|
+
if args.verb == "readiness-scan":
|
|
3703
|
+
sys.exit(_cmd_pdata_readiness_scan(args))
|
|
3646
3704
|
if args.verb == "dump":
|
|
3647
3705
|
sys.exit(_cmd_pdata_dump(args))
|
|
3648
3706
|
if args.verb == "rehydrate":
|
|
@@ -14,7 +14,6 @@ import os
|
|
|
14
14
|
import shutil
|
|
15
15
|
import sqlite3
|
|
16
16
|
import subprocess
|
|
17
|
-
import textwrap
|
|
18
17
|
import time
|
|
19
18
|
from dataclasses import dataclass
|
|
20
19
|
from enum import Enum
|
|
@@ -29,6 +28,7 @@ from cc_session_tools.lib.pdata.init_paths import (
|
|
|
29
28
|
MIGRATED_ARCHIVE_DIRNAME,
|
|
30
29
|
MIGRATED_MANIFEST_FILENAME,
|
|
31
30
|
)
|
|
31
|
+
from cc_session_tools.lib.text_wrap import wrap_block
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
from cc_session_tools.lib.install_sync import FailedAttempt
|
|
@@ -1224,12 +1224,11 @@ def _wrap_reason(reason: str, *, indent: str = " ") -> str:
|
|
|
1224
1224
|
embedded paths and remediation commands); printed on a single line they
|
|
1225
1225
|
hard-wrap at the terminal edge with no continuation indent, making a
|
|
1226
1226
|
wrapped reason indistinguishable from a new, unrelated result. Wrapping
|
|
1227
|
-
to the real terminal width
|
|
1227
|
+
to the real terminal width - falling back to 80 columns for non-tty
|
|
1228
1228
|
output (piped, redirected, or the scheduled drift-monitor job whose
|
|
1229
|
-
stdout is forwarded to Telegram)
|
|
1229
|
+
stdout is forwarded to Telegram) - and indenting every line fixes that.
|
|
1230
1230
|
"""
|
|
1231
|
-
|
|
1232
|
-
return textwrap.fill(reason, width=width, initial_indent=indent, subsequent_indent=indent)
|
|
1231
|
+
return wrap_block(reason, indent=indent)
|
|
1233
1232
|
|
|
1234
1233
|
|
|
1235
1234
|
def format_results(results: list[CheckResult], *, show_all: bool = False) -> str:
|
|
@@ -1265,7 +1264,13 @@ def format_results(results: list[CheckResult], *, show_all: bool = False) -> str
|
|
|
1265
1264
|
lines.append(f"All {len(results)} checks OK.")
|
|
1266
1265
|
has_issues = any(r.status in (Status.WARN, Status.FAIL) for r in results)
|
|
1267
1266
|
if has_issues:
|
|
1268
|
-
lines.append("
|
|
1267
|
+
lines.append("")
|
|
1268
|
+
lines.append(
|
|
1269
|
+
wrap_block(
|
|
1270
|
+
"Tip: run `ccst install-everything --apply` to sync skills, hooks, shell, and CLAUDE.md",
|
|
1271
|
+
indent="",
|
|
1272
|
+
)
|
|
1273
|
+
)
|
|
1269
1274
|
if not show_all:
|
|
1270
1275
|
lines.append("\nTo see full doctor output use --all argument")
|
|
1271
1276
|
return "\n".join(lines)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Deterministic pdata-readiness scan of a project's CSV files (`ccst pdata readiness-scan`).
|
|
2
|
+
|
|
3
|
+
Reports the mechanical blockers a migration audit would otherwise find by reading: ragged rows,
|
|
4
|
+
mixed date formats, mixed separators, machine-specific paths, fake nulls, duplicates, BOM/CRLF and
|
|
5
|
+
unreadable files, plus a per-file inventory and unique-column facts. Read-only: the scan never
|
|
6
|
+
writes. Findings carry counts and 1-based data-record numbers (header excluded), never cell text;
|
|
7
|
+
`unreadable` reasons are a fixed vocabulary, so file content cannot reach a report.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import csv
|
|
12
|
+
import io
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from cc_session_tools.lib.pdata.init_paths import EXCLUDED_DIR_NAMES
|
|
21
|
+
|
|
22
|
+
FINDING_KINDS: tuple[str, ...] = (
|
|
23
|
+
"ragged-rows", "machine-paths", "null-strings", "duplicate-rows", "comment-rows",
|
|
24
|
+
"repeated-header", "mixed-date-formats", "mixed-separators", "bad-header",
|
|
25
|
+
"no-unique-column", "unreadable", "bom", "crlf",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
MAX_EXAMPLE_ROWS = 3
|
|
29
|
+
FREE_TEXT_MEAN_LENGTH = 64
|
|
30
|
+
NULL_STRINGS = frozenset({"NO DATA", "N/A", "NA", "NULL", "NONE", "TBD", "UNKNOWN", "-", "--", "?"})
|
|
31
|
+
_MACHINE_PATH = re.compile(r"/Users/[^/\s]+|/home/[^/\s]+|/mnt/[a-z]/|[A-Za-z]:\\Users\\")
|
|
32
|
+
_DATE_FORMATS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
33
|
+
("iso-datetime", re.compile(r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}")),
|
|
34
|
+
("iso-date", re.compile(r"^\d{4}-\d{2}-\d{2}$")),
|
|
35
|
+
("slash", re.compile(r"^\d{1,2}/\d{1,2}/\d{2,4}$")),
|
|
36
|
+
("d-month-yyyy", re.compile(r"^\d{1,2} [A-Za-z]{3,9} \d{4}$")),
|
|
37
|
+
("month-d-yyyy", re.compile(r"^[A-Za-z]{3,9} \d{1,2},? \d{4}$")),
|
|
38
|
+
)
|
|
39
|
+
_BOM = b"\xef\xbb\xbf"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Finding:
|
|
44
|
+
kind: str
|
|
45
|
+
file: str
|
|
46
|
+
column: str | None = None
|
|
47
|
+
count: int | None = None
|
|
48
|
+
rows: tuple[int, ...] = ()
|
|
49
|
+
detail: tuple[tuple[str, int | str], ...] = ()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class UniqueColumn:
|
|
54
|
+
name: str
|
|
55
|
+
labels: tuple[str, ...]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class FileReport:
|
|
60
|
+
path: str
|
|
61
|
+
rows: int
|
|
62
|
+
columns: int
|
|
63
|
+
header: tuple[str, ...]
|
|
64
|
+
unique_columns: tuple[UniqueColumn, ...]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class ReadinessReport:
|
|
69
|
+
files: tuple[FileReport, ...]
|
|
70
|
+
findings: tuple[Finding, ...]
|
|
71
|
+
not_scanned: dict[str, int]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _Unreadable(Exception):
|
|
75
|
+
def __init__(self, reason: str) -> None:
|
|
76
|
+
super().__init__(reason)
|
|
77
|
+
self.reason = reason
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_real_yyyymmdd(value: str) -> bool:
|
|
81
|
+
if not re.fullmatch(r"\d{8}", value):
|
|
82
|
+
return False
|
|
83
|
+
try:
|
|
84
|
+
parsed = datetime.strptime(value, "%Y%m%d")
|
|
85
|
+
except ValueError:
|
|
86
|
+
return False
|
|
87
|
+
return 1900 <= parsed.year <= 2100
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _date_format(value: str) -> str | None:
|
|
91
|
+
for name, pattern in _DATE_FORMATS:
|
|
92
|
+
if pattern.match(value):
|
|
93
|
+
return name
|
|
94
|
+
return "yyyymmdd" if _is_real_yyyymmdd(value) else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _walk(root: Path) -> list[Path]:
|
|
98
|
+
"""Every file under root outside EXCLUDED_DIR_NAMES, without following directory symlinks -
|
|
99
|
+
the same exclusion set `ccst pdata init`'s classifier uses, so coverage matches migration."""
|
|
100
|
+
found: list[Path] = []
|
|
101
|
+
for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
|
|
102
|
+
dirnames[:] = sorted(d for d in dirnames if d not in EXCLUDED_DIR_NAMES)
|
|
103
|
+
found.extend(Path(dirpath) / name for name in sorted(filenames))
|
|
104
|
+
return found
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse(raw: bytes) -> tuple[list[list[str]], bool, bool]:
|
|
108
|
+
if not raw:
|
|
109
|
+
raise _Unreadable("empty-file")
|
|
110
|
+
try:
|
|
111
|
+
text = raw.decode("utf-8-sig")
|
|
112
|
+
except UnicodeDecodeError:
|
|
113
|
+
raise _Unreadable("not-utf8") from None
|
|
114
|
+
if not text.strip():
|
|
115
|
+
raise _Unreadable("empty-file")
|
|
116
|
+
try:
|
|
117
|
+
records = list(csv.reader(io.StringIO(text, newline=""), strict=True))
|
|
118
|
+
except csv.Error as exc:
|
|
119
|
+
raise _Unreadable(
|
|
120
|
+
"field-too-large" if "field larger" in str(exc) else "csv-parse-error"
|
|
121
|
+
) from None
|
|
122
|
+
return records, raw.startswith(_BOM), b"\r\n" in raw
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _scan_file(rel: str, raw: bytes) -> tuple[FileReport | None, list[Finding]]:
|
|
126
|
+
try:
|
|
127
|
+
records, has_bom, has_crlf = _parse(raw)
|
|
128
|
+
except _Unreadable as exc:
|
|
129
|
+
return None, [Finding("unreadable", rel, detail=(("reason", exc.reason),))]
|
|
130
|
+
|
|
131
|
+
findings: list[Finding] = []
|
|
132
|
+
if has_bom:
|
|
133
|
+
findings.append(Finding("bom", rel))
|
|
134
|
+
if has_crlf:
|
|
135
|
+
findings.append(Finding("crlf", rel))
|
|
136
|
+
|
|
137
|
+
header = records[0]
|
|
138
|
+
ncols = len(header)
|
|
139
|
+
if any(not h.strip() for h in header) or len(set(header)) != ncols:
|
|
140
|
+
findings.append(Finding("bad-header", rel))
|
|
141
|
+
|
|
142
|
+
data = [r for r in records[1:] if r]
|
|
143
|
+
nonempty = [0] * ncols
|
|
144
|
+
lengths = [0] * ncols
|
|
145
|
+
distinct: list[set[str]] = [set() for _ in range(ncols)]
|
|
146
|
+
all_int = [True] * ncols
|
|
147
|
+
dates: list[dict[str, int]] = [{} for _ in range(ncols)]
|
|
148
|
+
seps: list[dict[str, int]] = [{} for _ in range(ncols)]
|
|
149
|
+
nulls: list[dict[str, int]] = [{} for _ in range(ncols)]
|
|
150
|
+
null_rows: list[list[int]] = [[] for _ in range(ncols)]
|
|
151
|
+
path_rows: list[list[int]] = [[] for _ in range(ncols)]
|
|
152
|
+
ragged: list[int] = []
|
|
153
|
+
dup_rows: list[int] = []
|
|
154
|
+
comment_rows: list[int] = []
|
|
155
|
+
header_rows: list[int] = []
|
|
156
|
+
seen_rows: set[tuple[str, ...]] = set()
|
|
157
|
+
|
|
158
|
+
for number, row in enumerate(data, start=1):
|
|
159
|
+
if row == header:
|
|
160
|
+
header_rows.append(number)
|
|
161
|
+
if row[0].startswith("#"):
|
|
162
|
+
comment_rows.append(number)
|
|
163
|
+
if len(row) != ncols:
|
|
164
|
+
ragged.append(number)
|
|
165
|
+
key = tuple(row)
|
|
166
|
+
if key in seen_rows:
|
|
167
|
+
dup_rows.append(number)
|
|
168
|
+
seen_rows.add(key)
|
|
169
|
+
for i, value in enumerate(row[:ncols]):
|
|
170
|
+
if not value.strip():
|
|
171
|
+
continue
|
|
172
|
+
nonempty[i] += 1
|
|
173
|
+
lengths[i] += len(value)
|
|
174
|
+
distinct[i].add(value)
|
|
175
|
+
if not re.fullmatch(r"-?\d+", value.strip()):
|
|
176
|
+
all_int[i] = False
|
|
177
|
+
fmt = _date_format(value.strip())
|
|
178
|
+
if fmt:
|
|
179
|
+
dates[i][fmt] = dates[i].get(fmt, 0) + 1
|
|
180
|
+
for sep in ("|", ";"):
|
|
181
|
+
if sep in value:
|
|
182
|
+
seps[i][sep] = seps[i].get(sep, 0) + 1
|
|
183
|
+
token = value.strip().upper()
|
|
184
|
+
if token in NULL_STRINGS:
|
|
185
|
+
nulls[i][token] = nulls[i].get(token, 0) + 1
|
|
186
|
+
null_rows[i].append(number)
|
|
187
|
+
if _MACHINE_PATH.search(value):
|
|
188
|
+
path_rows[i].append(number)
|
|
189
|
+
|
|
190
|
+
def row_finding(kind: str, rows: list[int], column: str | None = None,
|
|
191
|
+
detail: tuple[tuple[str, int | str], ...] = ()) -> Finding:
|
|
192
|
+
return Finding(kind, rel, column, len(rows), tuple(rows[:MAX_EXAMPLE_ROWS]), detail)
|
|
193
|
+
|
|
194
|
+
if ragged:
|
|
195
|
+
findings.append(row_finding("ragged-rows", ragged))
|
|
196
|
+
if dup_rows:
|
|
197
|
+
findings.append(row_finding("duplicate-rows", dup_rows))
|
|
198
|
+
if comment_rows:
|
|
199
|
+
findings.append(row_finding("comment-rows", comment_rows))
|
|
200
|
+
if header_rows:
|
|
201
|
+
findings.append(row_finding("repeated-header", header_rows))
|
|
202
|
+
for i, name in enumerate(header):
|
|
203
|
+
if len(dates[i]) >= 2:
|
|
204
|
+
findings.append(Finding("mixed-date-formats", rel, name,
|
|
205
|
+
detail=tuple(sorted(dates[i].items()))))
|
|
206
|
+
if len(seps[i]) == 2:
|
|
207
|
+
findings.append(Finding("mixed-separators", rel, name,
|
|
208
|
+
detail=tuple(sorted(seps[i].items()))))
|
|
209
|
+
if path_rows[i]:
|
|
210
|
+
findings.append(row_finding("machine-paths", path_rows[i], name))
|
|
211
|
+
if null_rows[i]:
|
|
212
|
+
findings.append(row_finding("null-strings", null_rows[i], name,
|
|
213
|
+
tuple(sorted(nulls[i].items()))))
|
|
214
|
+
|
|
215
|
+
unique: list[UniqueColumn] = []
|
|
216
|
+
if len(data) >= 2:
|
|
217
|
+
for i, name in enumerate(header):
|
|
218
|
+
if nonempty[i] == len(data) and len(distinct[i]) == len(data):
|
|
219
|
+
labels: list[str] = []
|
|
220
|
+
if all_int[i]:
|
|
221
|
+
labels.append("integer-like")
|
|
222
|
+
if lengths[i] / len(data) > FREE_TEXT_MEAN_LENGTH:
|
|
223
|
+
labels.append("free-text")
|
|
224
|
+
unique.append(UniqueColumn(name, tuple(labels)))
|
|
225
|
+
if not unique:
|
|
226
|
+
findings.append(Finding("no-unique-column", rel))
|
|
227
|
+
|
|
228
|
+
return FileReport(rel, len(data), ncols, tuple(header), tuple(unique)), findings
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def scan_project(root: Path, *, path_prefix: str | None = None) -> ReadinessReport:
|
|
232
|
+
files: list[FileReport] = []
|
|
233
|
+
findings: list[Finding] = []
|
|
234
|
+
not_scanned = {"json": 0, "markdown": 0, "other": 0}
|
|
235
|
+
for abs_path in _walk(root):
|
|
236
|
+
rel = abs_path.relative_to(root).as_posix()
|
|
237
|
+
if path_prefix and not rel.startswith(path_prefix):
|
|
238
|
+
continue
|
|
239
|
+
suffix = abs_path.suffix.lower()
|
|
240
|
+
if suffix != ".csv":
|
|
241
|
+
bucket = {".json": "json", ".md": "markdown", ".markdown": "markdown"}.get(suffix, "other")
|
|
242
|
+
not_scanned[bucket] += 1
|
|
243
|
+
continue
|
|
244
|
+
report, file_findings = _scan_file(rel, abs_path.read_bytes())
|
|
245
|
+
if report is not None:
|
|
246
|
+
files.append(report)
|
|
247
|
+
findings.extend(file_findings)
|
|
248
|
+
findings.sort(key=lambda f: (f.file, FINDING_KINDS.index(f.kind), f.column or ""))
|
|
249
|
+
return ReadinessReport(tuple(files), tuple(findings), not_scanned)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _finding_line(f: Finding) -> str:
|
|
253
|
+
parts = [f"- {f.kind}: {f.file}"]
|
|
254
|
+
if f.column is not None:
|
|
255
|
+
parts.append(f"column {f.column!r}")
|
|
256
|
+
if f.count is not None:
|
|
257
|
+
parts.append(f"count={f.count}")
|
|
258
|
+
if f.rows:
|
|
259
|
+
parts.append("rows=" + ",".join(str(r) for r in f.rows))
|
|
260
|
+
if f.detail:
|
|
261
|
+
parts.append(", ".join(f"{k}={v}" for k, v in f.detail))
|
|
262
|
+
return " ".join(parts)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def format_markdown(report: ReadinessReport, *, findings_only: bool = False) -> str:
|
|
266
|
+
ns = report.not_scanned
|
|
267
|
+
lines = [
|
|
268
|
+
"# pdata readiness scan",
|
|
269
|
+
"",
|
|
270
|
+
"Row numbers are 1-based data records (header excluded), not line numbers: a quoted "
|
|
271
|
+
"multiline cell makes them differ.",
|
|
272
|
+
f"Scanned {len(report.files)} CSV file(s); not scanned: {ns['json']} JSON, "
|
|
273
|
+
f"{ns['markdown']} Markdown, {ns['other']} other.",
|
|
274
|
+
]
|
|
275
|
+
if not report.files and not any(f.kind == "unreadable" for f in report.findings):
|
|
276
|
+
lines += ["", "No CSV files found."]
|
|
277
|
+
lines += ["", "## Findings"]
|
|
278
|
+
lines += [_finding_line(f) for f in report.findings] or ["No findings."]
|
|
279
|
+
if not findings_only and report.files:
|
|
280
|
+
lines += [
|
|
281
|
+
"",
|
|
282
|
+
"## Inventory",
|
|
283
|
+
"A unique column is a fact, not a recommended key: it is not a stable key until a "
|
|
284
|
+
"human has confirmed its values are not re-numbered per session or machine.",
|
|
285
|
+
]
|
|
286
|
+
for f in report.files:
|
|
287
|
+
unique = ", ".join(
|
|
288
|
+
u.name + (f" ({'/'.join(u.labels)})" if u.labels else "") for u in f.unique_columns
|
|
289
|
+
)
|
|
290
|
+
lines.append(
|
|
291
|
+
f"- {f.path}: {f.rows} rows, {f.columns} columns"
|
|
292
|
+
+ (f"; unique: {unique}" if unique else "")
|
|
293
|
+
)
|
|
294
|
+
return "\n".join(lines)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def format_json(report: ReadinessReport) -> str:
|
|
298
|
+
return json.dumps(
|
|
299
|
+
{
|
|
300
|
+
"files": [
|
|
301
|
+
{
|
|
302
|
+
"path": f.path, "rows": f.rows, "columns": f.columns,
|
|
303
|
+
"header": list(f.header),
|
|
304
|
+
"unique_columns": [{"name": u.name, "labels": list(u.labels)}
|
|
305
|
+
for u in f.unique_columns],
|
|
306
|
+
}
|
|
307
|
+
for f in report.files
|
|
308
|
+
],
|
|
309
|
+
"findings": [
|
|
310
|
+
{
|
|
311
|
+
"kind": f.kind, "file": f.file, "column": f.column, "count": f.count,
|
|
312
|
+
"rows": list(f.rows), "detail": dict(f.detail),
|
|
313
|
+
}
|
|
314
|
+
for f in report.findings
|
|
315
|
+
],
|
|
316
|
+
"not_scanned": report.not_scanned,
|
|
317
|
+
},
|
|
318
|
+
indent=2,
|
|
319
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Terminal-width word wrapping shared by `ccst doctor` and the hooks install output."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
import textwrap
|
|
6
|
+
|
|
7
|
+
FALLBACK_WIDTH = 80
|
|
8
|
+
MIN_WIDTH = 40
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def effective_width() -> int:
|
|
12
|
+
"""Width to wrap to: the detected terminal width, never below MIN_WIDTH.
|
|
13
|
+
|
|
14
|
+
`shutil.get_terminal_size` reads `COLUMNS`, then the real terminal via the process's stdout
|
|
15
|
+
file descriptor (so an in-process `redirect_stdout` capture still sees the real terminal),
|
|
16
|
+
and falls back to FALLBACK_WIDTH when neither is available. It is called through the module
|
|
17
|
+
attribute so tests can patch it.
|
|
18
|
+
"""
|
|
19
|
+
return max(shutil.get_terminal_size(fallback=(FALLBACK_WIDTH, 24)).columns, MIN_WIDTH)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def wrap_block(text: str, *, indent: str = " ") -> str:
|
|
23
|
+
"""Word-wrap `text` to the effective width, indenting every line.
|
|
24
|
+
|
|
25
|
+
Words are never split: a path, hyphenated name or backticked command that has no whitespace
|
|
26
|
+
stays on one line, and a single token longer than the width overflows rather than breaking.
|
|
27
|
+
"""
|
|
28
|
+
return textwrap.fill(
|
|
29
|
+
text,
|
|
30
|
+
width=effective_width(),
|
|
31
|
+
initial_indent=indent,
|
|
32
|
+
subsequent_indent=indent,
|
|
33
|
+
break_long_words=False,
|
|
34
|
+
break_on_hyphens=False,
|
|
35
|
+
)
|