@pennyfarthing/core 10.0.3 → 10.1.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/README.md +9 -7
- package/package.json +7 -1
- package/packages/core/dist/cli/commands/cyclist.d.ts +5 -1
- package/packages/core/dist/cli/commands/cyclist.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/cyclist.js +4 -4
- package/packages/core/dist/cli/commands/cyclist.js.map +1 -1
- package/packages/core/dist/cli/commands/cyclist.test.js +2 -2
- package/packages/core/dist/cli/commands/cyclist.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor-legacy.test.js +17 -16
- package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +251 -4
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.d.ts +7 -0
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +43 -7
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +26 -0
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/index.js +1 -1
- package/packages/core/dist/cli/index.js.map +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
- package/packages/core/dist/cli/utils/files.d.ts +10 -0
- package/packages/core/dist/cli/utils/files.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/files.js +35 -0
- package/packages/core/dist/cli/utils/files.js.map +1 -1
- package/packages/core/dist/cli/utils/python.d.ts +22 -0
- package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/python.js +102 -0
- package/packages/core/dist/cli/utils/python.js.map +1 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +10 -0
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-report.js +11 -7
- package/packages/core/dist/scripts/generate-report.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider-report.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-spider-report.js +12 -8
- package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-spider.js +6 -4
- package/packages/core/dist/scripts/generate-spider.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider.test.js +2 -2
- package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
- package/pennyfarthing-dist/agents/README.md +1 -3
- package/pennyfarthing-dist/agents/architect.md +0 -6
- package/pennyfarthing-dist/agents/devops.md +0 -6
- package/pennyfarthing-dist/agents/orchestrator.md +0 -6
- package/pennyfarthing-dist/agents/pm.md +1 -7
- package/pennyfarthing-dist/agents/sm-finish.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +2 -2
- package/pennyfarthing-dist/agents/sm.md +4 -11
- package/pennyfarthing-dist/commands/architect.md +11 -3
- package/pennyfarthing-dist/commands/close-epic.md +24 -131
- package/pennyfarthing-dist/commands/create-theme.md +14 -24
- package/pennyfarthing-dist/commands/dev.md +11 -3
- package/pennyfarthing-dist/commands/devops.md +11 -3
- package/pennyfarthing-dist/commands/health-check.md +1 -3
- package/pennyfarthing-dist/commands/help.md +8 -12
- package/pennyfarthing-dist/commands/list-themes.md +14 -16
- package/pennyfarthing-dist/commands/orchestrator.md +11 -3
- package/pennyfarthing-dist/commands/parallel-work.md +1 -3
- package/pennyfarthing-dist/commands/pm.md +11 -3
- package/pennyfarthing-dist/commands/prime.md +6 -6
- package/pennyfarthing-dist/commands/repo-status.md +2 -2
- package/pennyfarthing-dist/commands/reviewer.md +11 -3
- package/pennyfarthing-dist/commands/run-ci.md +1 -1
- package/pennyfarthing-dist/commands/set-theme.md +14 -51
- package/pennyfarthing-dist/commands/setup.md +1 -1
- package/pennyfarthing-dist/commands/show-theme.md +14 -16
- package/pennyfarthing-dist/commands/sm.md +11 -3
- package/pennyfarthing-dist/commands/sprint.md +8 -8
- package/pennyfarthing-dist/commands/tea.md +11 -3
- package/pennyfarthing-dist/commands/tech-writer.md +11 -3
- package/pennyfarthing-dist/commands/theme-maker.md +14 -671
- package/pennyfarthing-dist/commands/theme.md +95 -0
- package/pennyfarthing-dist/commands/ux-designer.md +11 -3
- package/pennyfarthing-dist/commands/work.md +3 -5
- package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
- package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
- package/pennyfarthing-dist/guides/hooks.md +5 -5
- package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
- package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
- package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
- package/pennyfarthing-dist/guides/prime.md +2 -2
- package/pennyfarthing-dist/guides/skill-schema.md +25 -26
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +2 -2
- package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
- package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +1 -1
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +3 -3
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +14 -12
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/README.md +1 -1
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +3 -3
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
- package/pennyfarthing-dist/scripts/sprint/README.md +32 -17
- package/pennyfarthing-dist/scripts/story/README.md +1 -1
- package/pennyfarthing-dist/scripts/test/test-setup.sh +1 -1
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
- package/pennyfarthing-dist/scripts/theme/README.md +1 -1
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +62 -17
- package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
- package/pennyfarthing-dist/skills/skill-registry.yaml +41 -28
- package/pennyfarthing-dist/skills/sprint/skill.md +386 -68
- package/pennyfarthing-dist/skills/story/skill.md +14 -206
- package/pennyfarthing-dist/skills/theme/skill.md +290 -75
- package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
- package/pennyfarthing-dist/skills/workflow/skill.md +4 -4
- package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
- package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
- package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
- package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +42 -25
- package/pennyfarthing-dist/workflows/git-cleanup.yaml +1 -1
- package/pennyfarthing-dist/workflows/project-setup/steps/step-10-complete.md +1 -1
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/cli.py +15 -0
- package/pennyfarthing_scripts/codemarkers/__init__.py +19 -0
- package/pennyfarthing_scripts/codemarkers/__main__.py +6 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/analyze.py +326 -0
- package/pennyfarthing_scripts/codemarkers/cli.py +129 -0
- package/pennyfarthing_scripts/codemarkers/formatters.py +89 -0
- package/pennyfarthing_scripts/codemarkers/models.py +45 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +2 -1
- package/pennyfarthing_scripts/complexity/__init__.py +15 -0
- package/pennyfarthing_scripts/complexity/__main__.py +6 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/analyze.py +207 -0
- package/pennyfarthing_scripts/complexity/cli.py +78 -0
- package/pennyfarthing_scripts/complexity/formatters.py +64 -0
- package/pennyfarthing_scripts/complexity/models.py +32 -0
- package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__main__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/analyze.py +323 -0
- package/pennyfarthing_scripts/deadcode/cli.py +163 -0
- package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
- package/pennyfarthing_scripts/deadcode/models.py +54 -0
- package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
- package/pennyfarthing_scripts/dependencies/__main__.py +5 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/analyze.py +155 -0
- package/pennyfarthing_scripts/dependencies/cli.py +72 -0
- package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
- package/pennyfarthing_scripts/dependencies/models.py +39 -0
- package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
- package/pennyfarthing_scripts/healthscore/__main__.py +6 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/analyze.py +161 -0
- package/pennyfarthing_scripts/healthscore/cli.py +76 -0
- package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
- package/pennyfarthing_scripts/healthscore/models.py +44 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/analyze.py +28 -1
- package/pennyfarthing_scripts/hotspots/cli.py +11 -9
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +42 -15
- package/pennyfarthing_scripts/jira/cli.py +78 -1
- package/pennyfarthing_scripts/jira/client.py +28 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/workflow.py +5 -3
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +63 -6
- package/pennyfarthing_scripts/sprint/archive_epic.py +198 -85
- package/pennyfarthing_scripts/sprint/cli.py +1565 -65
- package/pennyfarthing_scripts/sprint/epic_add.py +173 -0
- package/pennyfarthing_scripts/sprint/loader.py +46 -2
- package/pennyfarthing_scripts/sprint/story_add.py +202 -27
- package/pennyfarthing_scripts/sprint/story_finish.py +211 -0
- package/pennyfarthing_scripts/sprint/validate_cmd.py +44 -5
- package/pennyfarthing_scripts/sprint/validator.py +13 -3
- package/pennyfarthing_scripts/sprint/work.py +43 -3
- package/pennyfarthing_scripts/sprint/yaml_io.py +124 -15
- package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_codemarkers.py +682 -0
- package/pennyfarthing_scripts/tests/test_healthscore.py +524 -0
- package/pennyfarthing_scripts/tests/test_sprint_package.py +166 -0
- package/pennyfarthing_scripts/tests/test_yaml_io.py +117 -0
- package/pennyfarthing_scripts/theme/__init__.py +5 -0
- package/pennyfarthing_scripts/theme/__main__.py +6 -0
- package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/cli.py +286 -0
- package/scripts/README.md +53 -0
- package/scripts/postinstall.cjs +34 -0
- package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +0 -133
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +0 -91
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +0 -158
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +0 -52
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +0 -63
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +0 -145
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +0 -110
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +0 -148
- package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +0 -415
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +0 -33
- package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +0 -230
- package/pennyfarthing-dist/scripts/sprint/sprint-status.sh +0 -134
- package/pennyfarthing-dist/scripts/sprint/validate-sprint-yaml.sh +0 -139
- package/pennyfarthing-dist/skills/sprint/scripts/archive-story.sh +0 -101
- package/pennyfarthing-dist/skills/sprint/scripts/available-stories.sh +0 -97
- package/pennyfarthing-dist/skills/sprint/scripts/check-story.sh +0 -164
- package/pennyfarthing-dist/skills/sprint/scripts/create-jira-epic.sh +0 -23
- package/pennyfarthing-dist/skills/sprint/scripts/new-sprint.sh +0 -116
- package/pennyfarthing-dist/skills/sprint/scripts/promote-epic.sh +0 -164
- package/pennyfarthing-dist/skills/sprint/scripts/sprint-info.sh +0 -39
- package/pennyfarthing-dist/skills/sprint/scripts/sprint-status.sh +0 -147
- package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +0 -23
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core dependency analysis engine.
|
|
3
|
+
|
|
4
|
+
Wraps npm outdated --json and npm audit --json.
|
|
5
|
+
Parses output into models following ADR-0008 result pattern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from pennyfarthing_scripts.dependencies.models import (
|
|
17
|
+
OutdatedPackage,
|
|
18
|
+
SecurityAdvisory,
|
|
19
|
+
DependenciesResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _find_npm(target_path: Path) -> Path | None:
|
|
24
|
+
"""Find npm binary via shutil.which."""
|
|
25
|
+
npm = shutil.which("npm")
|
|
26
|
+
return Path(npm) if npm else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _check_package_json(target_path: Path) -> bool:
|
|
30
|
+
"""Check if package.json exists in target directory."""
|
|
31
|
+
return (target_path / "package.json").exists()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_outdated_output(output: str) -> list[OutdatedPackage]:
|
|
35
|
+
"""Parse npm outdated --json output into OutdatedPackage models.
|
|
36
|
+
|
|
37
|
+
npm outdated --json returns: {pkg_name: {current, wanted, latest, type, ...}}
|
|
38
|
+
"""
|
|
39
|
+
if not output:
|
|
40
|
+
return []
|
|
41
|
+
try:
|
|
42
|
+
data = json.loads(output)
|
|
43
|
+
except (json.JSONDecodeError, TypeError):
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
if not data or not isinstance(data, dict):
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
packages = []
|
|
50
|
+
for name, info in data.items():
|
|
51
|
+
packages.append(OutdatedPackage(
|
|
52
|
+
name=name,
|
|
53
|
+
current=info.get("current", ""),
|
|
54
|
+
wanted=info.get("wanted", ""),
|
|
55
|
+
latest=info.get("latest", ""),
|
|
56
|
+
type=info.get("type", ""),
|
|
57
|
+
))
|
|
58
|
+
return packages
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_audit_output(output: str) -> list[SecurityAdvisory]:
|
|
62
|
+
"""Parse npm audit --json output into SecurityAdvisory models.
|
|
63
|
+
|
|
64
|
+
npm audit --json returns: {vulnerabilities: {name: {severity, ...}}, metadata: ...}
|
|
65
|
+
Aggregates by severity level.
|
|
66
|
+
"""
|
|
67
|
+
if not output:
|
|
68
|
+
return []
|
|
69
|
+
try:
|
|
70
|
+
data = json.loads(output)
|
|
71
|
+
except (json.JSONDecodeError, TypeError):
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
vulns = data.get("vulnerabilities", {})
|
|
75
|
+
if not vulns:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
severity_counts: Counter[str] = Counter()
|
|
79
|
+
for info in vulns.values():
|
|
80
|
+
sev = info.get("severity", "unknown")
|
|
81
|
+
severity_counts[sev] += 1
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
SecurityAdvisory(severity=sev, count=count)
|
|
85
|
+
for sev, count in severity_counts.items()
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def _run_npm_outdated(npm_bin: Path, target_path: Path) -> tuple[str, str, int]:
|
|
90
|
+
"""Run npm outdated --json subprocess."""
|
|
91
|
+
proc = await asyncio.create_subprocess_exec(
|
|
92
|
+
str(npm_bin),
|
|
93
|
+
"outdated",
|
|
94
|
+
"--json",
|
|
95
|
+
cwd=str(target_path),
|
|
96
|
+
stdout=asyncio.subprocess.PIPE,
|
|
97
|
+
stderr=asyncio.subprocess.PIPE,
|
|
98
|
+
)
|
|
99
|
+
stdout, stderr = await proc.communicate()
|
|
100
|
+
return (
|
|
101
|
+
stdout.decode("utf-8", errors="replace"),
|
|
102
|
+
stderr.decode("utf-8", errors="replace"),
|
|
103
|
+
proc.returncode or 0,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def _run_npm_audit(npm_bin: Path, target_path: Path) -> tuple[str, str, int]:
|
|
108
|
+
"""Run npm audit --json subprocess."""
|
|
109
|
+
proc = await asyncio.create_subprocess_exec(
|
|
110
|
+
str(npm_bin),
|
|
111
|
+
"audit",
|
|
112
|
+
"--json",
|
|
113
|
+
cwd=str(target_path),
|
|
114
|
+
stdout=asyncio.subprocess.PIPE,
|
|
115
|
+
stderr=asyncio.subprocess.PIPE,
|
|
116
|
+
)
|
|
117
|
+
stdout, stderr = await proc.communicate()
|
|
118
|
+
return (
|
|
119
|
+
stdout.decode("utf-8", errors="replace"),
|
|
120
|
+
stderr.decode("utf-8", errors="replace"),
|
|
121
|
+
proc.returncode or 0,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def analyze_dependencies(target_path: Path) -> DependenciesResult:
|
|
126
|
+
"""Analyze dependencies of a Node.js project."""
|
|
127
|
+
resolved = target_path.resolve()
|
|
128
|
+
|
|
129
|
+
npm_bin = _find_npm(resolved)
|
|
130
|
+
if npm_bin is None:
|
|
131
|
+
return DependenciesResult(
|
|
132
|
+
success=False,
|
|
133
|
+
target_path=str(resolved),
|
|
134
|
+
error="npm not found. Install Node.js to use dependency analysis.",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not _check_package_json(resolved):
|
|
138
|
+
return DependenciesResult(
|
|
139
|
+
success=False,
|
|
140
|
+
target_path=str(resolved),
|
|
141
|
+
error="No package.json found in target directory.",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
outdated_stdout, _, _ = await _run_npm_outdated(npm_bin, resolved)
|
|
145
|
+
audit_stdout, _, _ = await _run_npm_audit(npm_bin, resolved)
|
|
146
|
+
|
|
147
|
+
outdated = _parse_outdated_output(outdated_stdout)
|
|
148
|
+
advisories = _parse_audit_output(audit_stdout)
|
|
149
|
+
|
|
150
|
+
return DependenciesResult(
|
|
151
|
+
success=True,
|
|
152
|
+
target_path=str(resolved),
|
|
153
|
+
outdated=outdated,
|
|
154
|
+
advisories=advisories,
|
|
155
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for dependency analysis.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m pennyfarthing_scripts.dependencies analyze [OPTIONS]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def dependencies():
|
|
18
|
+
"""Dependency staleness and security analysis.
|
|
19
|
+
|
|
20
|
+
\b
|
|
21
|
+
Commands:
|
|
22
|
+
analyze - Analyze dependency health
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run_analysis(target_path: str | None) -> "DependenciesResult":
|
|
28
|
+
"""Run analysis and return result."""
|
|
29
|
+
from pennyfarthing_scripts.dependencies.analyze import analyze_dependencies
|
|
30
|
+
|
|
31
|
+
p = Path(target_path).resolve() if target_path else Path(".").resolve()
|
|
32
|
+
return asyncio.run(analyze_dependencies(p))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _output_result(result, fmt: str, output_file: str | None):
|
|
36
|
+
"""Format and output the analysis result."""
|
|
37
|
+
from pennyfarthing_scripts.dependencies.formatters import (
|
|
38
|
+
format_outdated_table,
|
|
39
|
+
format_audit_table,
|
|
40
|
+
export_json,
|
|
41
|
+
export_csv,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if fmt == "json":
|
|
45
|
+
text = export_json(result)
|
|
46
|
+
elif fmt == "csv":
|
|
47
|
+
text = export_csv(result.outdated)
|
|
48
|
+
else:
|
|
49
|
+
parts = [format_outdated_table(result.outdated)]
|
|
50
|
+
if result.advisories:
|
|
51
|
+
parts.append("")
|
|
52
|
+
parts.append(format_audit_table(result.advisories))
|
|
53
|
+
text = "\n".join(parts)
|
|
54
|
+
|
|
55
|
+
if output_file:
|
|
56
|
+
Path(output_file).write_text(text)
|
|
57
|
+
click.echo(f"Output written to {output_file}", err=True)
|
|
58
|
+
else:
|
|
59
|
+
click.echo(text)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dependencies.command()
|
|
63
|
+
@click.option("--path", "target_path", type=click.Path(exists=True),
|
|
64
|
+
help="Directory to analyze")
|
|
65
|
+
@click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]),
|
|
66
|
+
default="table", show_default=True)
|
|
67
|
+
@click.option("--output", "output_file", type=click.Path(),
|
|
68
|
+
help="Write output to file")
|
|
69
|
+
def analyze(target_path, fmt, output_file):
|
|
70
|
+
"""Analyze dependency health."""
|
|
71
|
+
result = _run_analysis(target_path)
|
|
72
|
+
_output_result(result, fmt, output_file)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatters for dependency analysis results.
|
|
3
|
+
|
|
4
|
+
Supports table, JSON, and CSV output — no external dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
|
|
14
|
+
from pennyfarthing_scripts.dependencies.models import (
|
|
15
|
+
OutdatedPackage,
|
|
16
|
+
SecurityAdvisory,
|
|
17
|
+
DependenciesResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_outdated_table(packages: list[OutdatedPackage]) -> str:
|
|
22
|
+
"""Format outdated packages as column-aligned table."""
|
|
23
|
+
if not packages:
|
|
24
|
+
return " No outdated packages found."
|
|
25
|
+
|
|
26
|
+
hdr = f"{'Package':<30} {'Current':<12} {'Wanted':<12} {'Latest':<12} Type"
|
|
27
|
+
sep = f"{'-' * 30} {'-' * 12} {'-' * 12} {'-' * 12} ----"
|
|
28
|
+
|
|
29
|
+
lines = [hdr, sep]
|
|
30
|
+
for p in packages:
|
|
31
|
+
lines.append(
|
|
32
|
+
f"{p.name:<30} {p.current:<12} {p.wanted:<12} {p.latest:<12} {p.type}"
|
|
33
|
+
)
|
|
34
|
+
return "\n".join(lines)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_audit_table(advisories: list[SecurityAdvisory]) -> str:
|
|
38
|
+
"""Format security advisories as table."""
|
|
39
|
+
if not advisories:
|
|
40
|
+
return " No vulnerabilities found."
|
|
41
|
+
|
|
42
|
+
hdr = f"{'Severity':<12} {'Count':>6}"
|
|
43
|
+
sep = f"{'-' * 12} {'-' * 6}"
|
|
44
|
+
|
|
45
|
+
lines = [hdr, sep]
|
|
46
|
+
for a in advisories:
|
|
47
|
+
lines.append(f"{a.severity:<12} {a.count:>6}")
|
|
48
|
+
return "\n".join(lines)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def export_json(result: DependenciesResult) -> str:
|
|
52
|
+
"""Serialize result to JSON string."""
|
|
53
|
+
return json.dumps(asdict(result), indent=2, default=str)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def export_csv(packages: list[OutdatedPackage]) -> str:
|
|
57
|
+
"""Export outdated packages as CSV."""
|
|
58
|
+
buf = io.StringIO()
|
|
59
|
+
writer = csv.writer(buf)
|
|
60
|
+
writer.writerow(["name", "current", "wanted", "latest", "type"])
|
|
61
|
+
for p in packages:
|
|
62
|
+
writer.writerow([p.name, p.current, p.wanted, p.latest, p.type])
|
|
63
|
+
return buf.getvalue()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for dependency analysis results.
|
|
3
|
+
|
|
4
|
+
Follows ADR-0008 result pattern — structured dataclasses with success/error fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class OutdatedPackage:
|
|
14
|
+
"""A package with available updates."""
|
|
15
|
+
|
|
16
|
+
name: str = ""
|
|
17
|
+
current: str = ""
|
|
18
|
+
wanted: str = ""
|
|
19
|
+
latest: str = ""
|
|
20
|
+
type: str = ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SecurityAdvisory:
|
|
25
|
+
"""Aggregated vulnerability count per severity level."""
|
|
26
|
+
|
|
27
|
+
severity: str = ""
|
|
28
|
+
count: int = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DependenciesResult:
|
|
33
|
+
"""Analysis result following ADR-0008 pattern."""
|
|
34
|
+
|
|
35
|
+
success: bool = False
|
|
36
|
+
target_path: str = ""
|
|
37
|
+
outdated: list[OutdatedPackage] = field(default_factory=list)
|
|
38
|
+
advisories: list[SecurityAdvisory] = field(default_factory=list)
|
|
39
|
+
error: str | None = None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Composite health score for codebase analysis.
|
|
3
|
+
|
|
4
|
+
Aggregates 8 dimensions into a single 0-100 weighted score:
|
|
5
|
+
churn, TODO density, complexity, test gaps, dead code,
|
|
6
|
+
deprecation debt, dependency freshness, agent context efficiency.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pennyfarthing_scripts.healthscore.models import (
|
|
10
|
+
DimensionScore,
|
|
11
|
+
HealthscoreResult,
|
|
12
|
+
DEFAULT_WEIGHTS,
|
|
13
|
+
)
|
|
14
|
+
from pennyfarthing_scripts.healthscore.analyze import analyze_healthscore
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DimensionScore",
|
|
18
|
+
"HealthscoreResult",
|
|
19
|
+
"DEFAULT_WEIGHTS",
|
|
20
|
+
"analyze_healthscore",
|
|
21
|
+
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core health score analysis engine.
|
|
3
|
+
|
|
4
|
+
Aggregates lightweight dimension scores into a composite 0-100 score.
|
|
5
|
+
Supports caching with a configurable TTL (default 5 minutes).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pennyfarthing_scripts.healthscore.models import (
|
|
16
|
+
DEFAULT_WEIGHTS,
|
|
17
|
+
DimensionScore,
|
|
18
|
+
HealthscoreResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def analyze_healthscore(
|
|
23
|
+
target_path: Path,
|
|
24
|
+
weights: dict[str, float] | None = None,
|
|
25
|
+
cache_ttl: int = 300,
|
|
26
|
+
) -> HealthscoreResult:
|
|
27
|
+
"""Analyze codebase health across all dimensions.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
target_path: Directory to analyze.
|
|
31
|
+
weights: Custom dimension weights (must sum to 1.0). Uses defaults if None.
|
|
32
|
+
cache_ttl: Cache time-to-live in seconds (default 300 = 5 minutes).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
HealthscoreResult with composite score and per-dimension breakdown.
|
|
36
|
+
"""
|
|
37
|
+
w = weights if weights is not None else DEFAULT_WEIGHTS
|
|
38
|
+
resolved = target_path.resolve()
|
|
39
|
+
|
|
40
|
+
cache_dir = get_cache_path(resolved)
|
|
41
|
+
any_cached = False
|
|
42
|
+
raw_scores: dict[str, float | None] = {}
|
|
43
|
+
dimensions: list[DimensionScore] = []
|
|
44
|
+
|
|
45
|
+
for dim_name, dim_weight in w.items():
|
|
46
|
+
score: float | None = None
|
|
47
|
+
error: str | None = None
|
|
48
|
+
|
|
49
|
+
# Try cache if ttl > 0
|
|
50
|
+
if cache_ttl > 0:
|
|
51
|
+
cached = read_cached_score(cache_dir, dim_name, cache_ttl)
|
|
52
|
+
if cached is not None:
|
|
53
|
+
score = cached
|
|
54
|
+
any_cached = True
|
|
55
|
+
|
|
56
|
+
# If no cached value, run lightweight probe
|
|
57
|
+
if score is None:
|
|
58
|
+
score = _probe_dimension(dim_name, resolved)
|
|
59
|
+
# Cache result if we got one and caching is enabled
|
|
60
|
+
if score is not None and cache_ttl > 0:
|
|
61
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
write_cached_score(cache_dir, dim_name, score)
|
|
63
|
+
|
|
64
|
+
if score is None:
|
|
65
|
+
error = f"{dim_name} not available"
|
|
66
|
+
|
|
67
|
+
raw_scores[dim_name] = score
|
|
68
|
+
dimensions.append(DimensionScore(
|
|
69
|
+
name=dim_name,
|
|
70
|
+
score=score,
|
|
71
|
+
weight=dim_weight,
|
|
72
|
+
error=error,
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
composite = compute_composite_score(raw_scores, w)
|
|
76
|
+
|
|
77
|
+
return HealthscoreResult(
|
|
78
|
+
success=True,
|
|
79
|
+
composite_score=composite,
|
|
80
|
+
target_path=str(resolved),
|
|
81
|
+
dimensions=dimensions,
|
|
82
|
+
cached=any_cached,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _probe_dimension(name: str, target_path: Path) -> float | None:
|
|
87
|
+
"""Run a lightweight probe for a single dimension.
|
|
88
|
+
|
|
89
|
+
Returns a score 0-100 or None if the dimension cannot be assessed.
|
|
90
|
+
These are intentionally simple heuristics — full analysis is deferred
|
|
91
|
+
to each dimension's own module when available.
|
|
92
|
+
"""
|
|
93
|
+
# For now, return None for all dimensions.
|
|
94
|
+
# Each dimension will be wired to its respective analyzer in future stories.
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def compute_composite_score(
|
|
99
|
+
dimension_scores: dict[str, float | None],
|
|
100
|
+
weights: dict[str, float],
|
|
101
|
+
) -> float:
|
|
102
|
+
"""Compute weighted average from dimension scores.
|
|
103
|
+
|
|
104
|
+
Dimensions with None scores are excluded and remaining weights
|
|
105
|
+
are renormalized.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
dimension_scores: Map of dimension name to score (0-100 or None).
|
|
109
|
+
weights: Map of dimension name to weight.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Composite score 0-100.
|
|
113
|
+
"""
|
|
114
|
+
total_weight = 0.0
|
|
115
|
+
weighted_sum = 0.0
|
|
116
|
+
|
|
117
|
+
for name, score in dimension_scores.items():
|
|
118
|
+
if score is not None:
|
|
119
|
+
w = weights.get(name, 0.0)
|
|
120
|
+
weighted_sum += score * w
|
|
121
|
+
total_weight += w
|
|
122
|
+
|
|
123
|
+
if total_weight == 0.0:
|
|
124
|
+
return 0.0
|
|
125
|
+
|
|
126
|
+
return weighted_sum / total_weight
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_cache_path(target_path: Path) -> Path:
|
|
130
|
+
"""Return the cache directory for a given target path."""
|
|
131
|
+
path_hash = hashlib.md5(str(target_path).encode()).hexdigest()[:12]
|
|
132
|
+
return target_path / ".pennyfarthing" / ".cache" / "healthscore" / path_hash
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def read_cached_score(cache_dir: Path, dimension: str, ttl: int) -> float | None:
|
|
136
|
+
"""Read a cached dimension score if still valid.
|
|
137
|
+
|
|
138
|
+
Returns None if cache miss or expired.
|
|
139
|
+
"""
|
|
140
|
+
cache_file = cache_dir / f"{dimension}.json"
|
|
141
|
+
if not cache_file.exists():
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
data = json.loads(cache_file.read_text())
|
|
146
|
+
except (json.JSONDecodeError, OSError):
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
ts = data.get("timestamp", 0)
|
|
150
|
+
if ttl <= 0 or (time.time() - ts) > ttl:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
return data.get("score")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def write_cached_score(cache_dir: Path, dimension: str, score: float) -> None:
|
|
157
|
+
"""Write a dimension score to cache."""
|
|
158
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
cache_file = cache_dir / f"{dimension}.json"
|
|
160
|
+
data = {"score": score, "timestamp": time.time()}
|
|
161
|
+
cache_file.write_text(json.dumps(data))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for health score analysis.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pf healthscore analyze [OPTIONS]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def healthscore():
|
|
18
|
+
"""Composite codebase health score.
|
|
19
|
+
|
|
20
|
+
\b
|
|
21
|
+
Commands:
|
|
22
|
+
analyze - Compute health score across all dimensions
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _common_options(fn):
|
|
28
|
+
"""Shared options for healthscore commands."""
|
|
29
|
+
fn = click.option("--path", "target_path", type=click.Path(exists=True),
|
|
30
|
+
help="Directory to analyze")(fn)
|
|
31
|
+
fn = click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]),
|
|
32
|
+
default="table", show_default=True)(fn)
|
|
33
|
+
fn = click.option("--output", "output_file", type=click.Path(),
|
|
34
|
+
help="Write output to file")(fn)
|
|
35
|
+
fn = click.option("--no-cache", is_flag=True,
|
|
36
|
+
help="Bypass cache, force fresh analysis")(fn)
|
|
37
|
+
return fn
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _run_analysis(target_path: str | None, no_cache: bool) -> "HealthscoreResult":
|
|
41
|
+
"""Run analysis and return result."""
|
|
42
|
+
from pennyfarthing_scripts.healthscore.analyze import analyze_healthscore
|
|
43
|
+
|
|
44
|
+
p = Path(target_path).resolve() if target_path else Path(".").resolve()
|
|
45
|
+
cache_ttl = 0 if no_cache else 300
|
|
46
|
+
return asyncio.run(analyze_healthscore(p, cache_ttl=cache_ttl))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _output_result(result, fmt: str, output_file: str | None):
|
|
50
|
+
"""Format and output the analysis result."""
|
|
51
|
+
from pennyfarthing_scripts.healthscore.formatters import (
|
|
52
|
+
export_csv,
|
|
53
|
+
export_json,
|
|
54
|
+
format_table,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if fmt == "json":
|
|
58
|
+
text = export_json(result)
|
|
59
|
+
elif fmt == "csv":
|
|
60
|
+
text = export_csv(result)
|
|
61
|
+
else:
|
|
62
|
+
text = format_table(result)
|
|
63
|
+
|
|
64
|
+
if output_file:
|
|
65
|
+
Path(output_file).write_text(text)
|
|
66
|
+
click.echo(f"Output written to {output_file}", err=True)
|
|
67
|
+
else:
|
|
68
|
+
click.echo(text)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@healthscore.command()
|
|
72
|
+
@_common_options
|
|
73
|
+
def analyze(target_path, fmt, output_file, no_cache):
|
|
74
|
+
"""Compute composite health score."""
|
|
75
|
+
result = _run_analysis(target_path, no_cache)
|
|
76
|
+
_output_result(result, fmt, output_file)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatters for health score results.
|
|
3
|
+
|
|
4
|
+
Supports table, JSON, and CSV output — no external dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
|
|
14
|
+
from pennyfarthing_scripts.healthscore.models import HealthscoreResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_table(result: HealthscoreResult) -> str:
|
|
18
|
+
"""Format health score results as column-aligned table."""
|
|
19
|
+
lines = [
|
|
20
|
+
f"Health Score: {result.composite_score:.1f} / 100",
|
|
21
|
+
"",
|
|
22
|
+
f"{'Dimension':<30} {'Score':>6} {'Weight':>6}",
|
|
23
|
+
f"{'-' * 30} {'------':>6} {'------':>6}",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
for dim in result.dimensions:
|
|
27
|
+
score_str = f"{dim.score:.1f}" if dim.score is not None else "N/A"
|
|
28
|
+
weight_pct = f"{dim.weight * 100:.0f}%"
|
|
29
|
+
lines.append(f"{dim.name:<30} {score_str:>6} {weight_pct:>6}")
|
|
30
|
+
|
|
31
|
+
return "\n".join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def export_json(result: HealthscoreResult) -> str:
|
|
35
|
+
"""Serialize result to JSON string."""
|
|
36
|
+
return json.dumps(asdict(result), indent=2, default=str)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def export_csv(result: HealthscoreResult) -> str:
|
|
40
|
+
"""Export dimension scores as CSV."""
|
|
41
|
+
buf = io.StringIO()
|
|
42
|
+
writer = csv.writer(buf)
|
|
43
|
+
writer.writerow(["dimension", "score", "weight", "error"])
|
|
44
|
+
for dim in result.dimensions:
|
|
45
|
+
writer.writerow([dim.name, dim.score, dim.weight, dim.error or ""])
|
|
46
|
+
return buf.getvalue()
|