公开记录
软件健康报告模式 0.27.0 · 指标 1.13.0 · 2026-07-28 09:55 UTC

dereuromark / cakephp-audit-stash

Flexible and rock solid audit log tracking for CakePHP

PHPMIT★ 8 星标⑂ 2 复刻始于 2025年10月在 GitHub 上查看 ↗

dereuromark/cakephp-audit-stash 的健康指数为 100 分中的 69 分,处于「中等」区间。 其得分最高的类别是Engineering Quality(94/100),最低的是Security(42/100)。 最近一次更新在 29 天前。 近期的大部分工作由 1 位贡献者完成。

69
总分 / 100
中等

软件健康指数

指标归入加权类别,统一采用 1–100 量表。总体分先取类别加权平均;当公开证据触发高风险司法辖区政策时,评级会按政策调整,并设置 49(有风险)的上限。AI 就绪度不计入总体分。

69
优秀85-100堪称典范;基本满足所有检验标准
良好70-84健康;仅有轻微不足
中等50-69可接受,但存在明显不足;建议进行审查
存在风险30-49存在重大薄弱环节;采用时应保持审慎
危急1-29问题严重(项目被弃置、仅有单一维护者、缺乏基本工程规范)
活力社区与采用可持续性与治理工程质量安全AI 就绪度

评分画像

每条轴代表一个类别。形状比平均值更重要——健康的对象会填满整个图形,而“一峰一谷”式画像意味着某一维度的优势正掩盖另一维度的风险。

所有权

Mark Scherer个人账户
750 关注者123 个公开仓库始于 2008年12月Available as (Cake)PHP expert

该仓库由个人账户拥有。相较于组织支持的项目,单一所有者项目的延续性风险更高。

软件包生态系统

注册表软件包版本月下载量版本数最近发布标签
Packagistdereuromark/cakephp-audit-stash2.1.06,8142639 天前loggingplugincakephpauditadmin-paneltrackinghistorystandalone-ui

按类别列示的指标

活力

项目是否仍有生命——是否仍在编写代码,是否仍在发布版本?

86优秀 · 占总体的 22%
评分方式
28.8/36推送新近度 — 最近一次推送于 29 天前
18.7/36提交节奏 — 52 周中有 27 周有提交
18/18提交量 — 最近一年 194 次提交
10/10OpenSSF Scorecard:Maintained — 30 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 10
所用输入
commits_last_year194
human_commit_share1
days_since_last_push29
active_weeks_last_year27

发布纪律

100优秀
评分方式
27/27有发布版本 — 已发布 26 个发布版本
36/36发布时效 — 最近一次发布版本于 39 天前
27/27发布节奏 — 约每 9.1 天发布一次
0/10OpenSSF Scorecard:Signed-Releases — 无数据
所用输入
releases_count26
latest_release_tag2.1.0
releases_from_tags
days_since_latest_release39
mean_days_between_releases9.1
已排除计分(无数据或不适用):OpenSSF Scorecard:Signed-Releases。 其余权重已重新归一化。

社区与采用

项目是否拥有用户、下载量与关注度,并具备欢迎贡献者参与的配置?

44存在风险 · 占总体的 18%
评分方式
13.7/60星标 — 8 个星标
0/25复刻 — 2 个复刻
0/15关注者 — 1 位关注者
所用输入
forks2
stars8
watchers1
growth_stateunverified
growth_factor_pct100
growth_unverified_reasonno_history

社区健康

70良好
评分方式
22.5/22.5README
22.5/22.5许可证 — 可识别的许可证(MIT)
18/18CONTRIBUTING 指南
0/13.5行为准则
0/7.2议题模板
0/6.3PR 模板
所用输入
has_readme
has_license
has_contributing
has_issue_template
has_code_of_conduct
has_pull_request_template
评分方式
51.1/80月度下载量 — packagist 合计每月 6,814 次下载
4.7/20注册表被依赖数 — 4 个软件包依赖它
所用输入
packagesdereuromark/cakephp-audit-stash
dependents4
ecosystemspackagist
total_downloads28,731
monthly_downloads6,814

可持续性与治理

项目能否在其成员之外延续——巴士系数、响应能力、由谁支持,以及软件包的维护状况?

70良好 · 占总体的 24%
评分方式
9/54巴士系数 — 1 位贡献者贡献了半数提交
10.2/22.5提交分布 — 头号贡献者编写了 55% 的提交
13.5/13.5贡献者广度 — 18 位贡献者
10/10OpenSSF Scorecard:Contributors — project has 11 contributing companies or organizations
所用输入
bus_factor1
contributors_sampled18
top_contributor_share0.546
评分方式
42.9/46.8议题解决 — 92% 的议题已关闭
36.6/38.3PR 接受 — 已裁定的 PR 中 65/68 已合并
0/15OpenSSF Scorecard:Code-Review — Found 2/29 approved changesets -- score normalized to 0
所用输入
merged_prs65
open_issues1
closed_issues11
issue_closed_ratio0.917
closed_unmerged_prs3
评分方式
10/30所有权背书 — 个人(用户)账户
0/20已验证域名 — 不适用于个人账户
20.7/25所有者影响力 — dereuromark 有 750 位关注者
25/25既往记录 — 123 个公开仓库,账户约 17 年
所用输入
followers750
owner_typeUser
is_verified
owner_logindereuromark
public_repos123
account_age_days6,437
已排除计分(无数据或不适用):已验证域名。 其余权重已重新归一化。
评分方式
25/25已发布且可解析 — packagist 上有 1 个软件包
35/35发布时效 — 最近一次发布于 39 天前
20/20版本历史 — 26 个已发布版本
20/20未被弃用 — 活跃,未被弃用或撤回
所用输入
packagesdereuromark/cakephp-audit-stash
ecosystemspackagist
any_deprecated
min_days_since_publish39

工程质量

基础的工程与文档实践是否到位?

94优秀 · 占总体的 20%

工程实践

90优秀
评分方式
24/24CI 工作流 — 2 个工作流
24/24存在测试
16/16Linter 配置 — phpcs.xml, phpstan.neon
0/9.6Pre-commit 钩子
6.4/6.4.editorconfig
20/20OpenSSF Scorecard:CI-Tests — 15 out of 15 merged PRs checked by a CI test -- score normalized to 10
所用输入
has_ci
has_tests
has_editorconfig
has_linter_config
has_precommit_config

文档

100优秀
评分方式
30/30README
25/25文档目录
15/15文档 / 主页站点 — https://dereuromark.github.io/cakephp-audit-stash/
10/10仓库描述
10/10主题标签 — 6 个主题标签
10/10Wiki
所用输入
topicsaudit-log, audit-trail, logging, cakephp, cakephp-plugin, php
has_wiki
homepagehttps://dereuromark.github.io/cakephp-audit-stash/
has_readme
has_docs_dir
has_description

安全

可见的安全与供应链实践是否稳固,且不存在未解决的高风险司法辖区暴露?

42存在风险 · 占总体的 16%

安全态势

42存在风险
评分方式
7.5/7.5Binary-Artifacts — no binaries found in the repo
2.2/7.5Branch-Protection — branch protection is not maximal on development and all release branches
2.5/2.5CI-Tests — 15 out of 15 merged PRs checked by a CI test -- score normalized to 10
0/2.5CII-Best-Practices — no effort to earn an OpenSSF best practices badge detected
0/7.5Code-Review — Found 2/29 approved changesets -- score normalized to 0
2.5/2.5Contributors — project has 11 contributing companies or organizations
10/10Dangerous-Workflow — no dangerous workflow patterns detected
0/7.5Dependency-Update-Tool — no update tool detected
0/5Fuzzing — project is not fuzzed
2.5/2.5许可证 — license file detected
7.5/7.5Maintained — 30 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 10
0/5Packaging — 无数据
0.5/5Pinned-Dependencies — dependency not pinned by hash detected -- score normalized to 1
0/5SAST — SAST tool is not run on all commits -- score normalized to 0
0/5Security-Policy — security policy file not detected
0/7.5Signed-Releases — 无数据
0/7.5Token-Permissions — detected GitHub workflow tokens with excessive permissions
3.8/7.5Vulnerabilities — 5 existing vulnerabilities detected
所用输入
sourceopenssf_scorecard
checks_evaluated16
scorecard_versionv5.5.0
checks_inconclusive2
scorecard_aggregate4.2
已排除计分(无数据或不适用):packaging, signed_releases。 其余权重已重新归一化。

AI 就绪度

该仓库在多大程度上具备与 AI 编码代理协同开发与维护的条件?这是一枚独立的实验性徽章——权重为 0.0,因此单独呈现,不影响总体健康评分。

45存在风险 · 占总体的 0%
评分方式
0/45代理指令 — 没有 CLAUDE.md / AGENTS.md / 编辑器规则
0/15机器可读文档(llms.txt)
40/40可读的提交历史 — 100 次人类提交中有 79 次说明了意图(结构化标题或解释性正文)
所用输入
has_llms_txt
legible_history_share0.79
agent_instruction_files
agent_instruction_max_bytes
评分方式
0/18一条命令的引导启动
22/22自动化测试
11/11Lint / 格式化配置 — phpcs.xml, phpstan.neon
0/11静态类型检查
10/10可复现环境 — lockfile
0/10已体现的代理实践 — 最近 100 次提交中没有代理编写的提交
0/8自动化维护 — 未观察到自动依赖更新
1/10OpenSSF Scorecard:Pinned-Dependencies — dependency not pinned by hash detected -- score normalized to 1
所用输入
has_nix
has_tests
lockfilespackage-lock.json
has_dockerfile
typed_language
bootstrap_files
has_devcontainer
has_linter_config
typecheck_configs
agent_commit_share0
toolchain_manifests
dependency_bot_commit_share0
评分方式
0/45可类型检查的代码 — PHP,未配置类型检查
55/55可控的文件大小 — 采样的 157 个源文件中有 0 个超过 60KB
所用输入
primary_languagePHP
largest_source_bytes40,814
source_files_sampled157
oversized_source_files0

关键数据

8GitHub 星标
18贡献者
194最近 12 个月提交数
29距最近推送天数
26发布版本数
1巴士系数(bus factor)
1开放议题
npm, Packagist软件包生态系统数

数据采集警告

  • Star history unavailable: GitHub GraphQL error: Resource not accessible by personal access token
  • GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

更多细节

Star 与 Fork 历史 0 ★ / 2 ⇿
0Star
2Fork
7发布

每颗 star 和每个 fork 的添加时间,来自 GitHub 并按天汇总。累计增长位于其构成来源——每日新增——的正上方,二者可相互对照:稳定的自然增长与短暂的突增形态截然不同。当这一差别可被衡量时,它会作为增长真实性予以报告。

111222212026-042026-052026-05
主版本 1次版本 2修订 4
OpenSSF Scorecard 4.2 / 10
4.2综合

来自开源项目 OpenSSF Scorecard 的独立、工具无关的安全评估。每项检查奖励的是安全实践本身,而非特定供应商的工具。Scorecard 无法判定的检查项标记为 不适用,并从安全评分中剔除(绝不按零分计)。Scorecard v5.5.0 · 2026-07-28 09:55 UTC

10Binary-Artifactsno binaries found in the repo
3Branch-Protectionbranch protection is not maximal on development and all release branches
10CI-Tests15 out of 15 merged PRs checked by a CI test -- score normalized to 10
0CII-Best-Practicesno effort to earn an OpenSSF best practices badge detected
0Code-ReviewFound 2/29 approved changesets -- score normalized to 0
10Contributorsproject has 11 contributing companies or organizations
10Dangerous-Workflowno dangerous workflow patterns detected
0Dependency-Update-Toolno update tool detected
0Fuzzingproject is not fuzzed
10Licenselicense file detected
10Maintained30 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 10
不适用Packagingpackaging workflow not detected
1Pinned-Dependenciesdependency not pinned by hash detected -- score normalized to 1
0SASTSAST tool is not run on all commits -- score normalized to 0
0Security-Policysecurity policy file not detected
不适用Signed-Releasesno releases found
0Token-Permissionsdetected GitHub workflow tokens with excessive permissions
5Vulnerabilities5 existing vulnerabilities detected
直接依赖 2
注册表软件包版本约束清单文件
Packagistcakephp/orm^5.3.0composer.json
Packagistsebastian/diff^6.0 || ^7.0 || ^8.0 || ^9.0composer.json
全部依赖 未采集

本报告未能采集到解析后的依赖集合:GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

原始 JSON 报告 机器可读
{
  "data": {
    "repo": {
      "topics": [
        "audit-log",
        "audit-trail",
        "logging",
        "cakephp",
        "cakephp-plugin",
        "php"
      ],
      "is_fork": false,
      "size_kb": 757,
      "has_wiki": true,
      "homepage": "https://dereuromark.github.io/cakephp-audit-stash/",
      "languages": {
        "PHP": 876741
      },
      "pushed_at": "2026-06-28T10:52:52Z",
      "created_at": "2025-10-15T20:48:45Z",
      "owner_type": "User",
      "updated_at": "2026-06-28T10:52:54Z",
      "description": "Flexible and rock solid audit log tracking for CakePHP",
      "is_archived": false,
      "is_disabled": false,
      "license_spdx": "MIT",
      "default_branch": "master",
      "license_spdx_raw": "MIT",
      "primary_language": "PHP",
      "significant_languages": [
        "PHP"
      ]
    },
    "owner": {
      "blog": "https://www.dereuromark.de/",
      "name": "Mark Scherer",
      "type": "User",
      "login": "dereuromark",
      "company": "Available as (Cake)PHP expert",
      "location": "Berlin",
      "followers": 750,
      "avatar_url": "https://avatars.githubusercontent.com/u/39854?v=4",
      "created_at": "2008-12-11T16:33:44Z",
      "is_verified": null,
      "public_repos": 123,
      "account_age_days": 6437
    },
    "license": {
      "state": "standard",
      "spdx_id": "MIT",
      "raw_spdx": "MIT",
      "file_present": true,
      "scorecard_found": true,
      "profile_has_license": true
    },
    "activity": {
      "releases": [
        {
          "tag": "2.1.0",
          "kind": "minor",
          "published_at": "2026-06-18T15:47:13Z"
        },
        {
          "tag": "2.0.1",
          "kind": "patch",
          "published_at": "2026-05-25T23:53:39Z"
        },
        {
          "tag": "2.0.0",
          "kind": "major",
          "published_at": "2026-05-04T11:59:30Z"
        },
        {
          "tag": "1.2.3",
          "kind": "patch",
          "published_at": "2026-05-03T19:14:38Z"
        },
        {
          "tag": "1.2.2",
          "kind": "patch",
          "published_at": "2026-05-03T15:30:40Z"
        },
        {
          "tag": "1.2.1",
          "kind": "patch",
          "published_at": "2026-05-03T07:39:32Z"
        },
        {
          "tag": "1.2.0",
          "kind": "minor",
          "published_at": "2026-04-27T13:02:27Z"
        },
        {
          "tag": "1.1.1",
          "kind": "patch",
          "published_at": "2026-04-17T16:36:20Z"
        },
        {
          "tag": "1.1.0",
          "kind": "minor",
          "published_at": "2026-04-17T11:56:52Z"
        },
        {
          "tag": "1.0.1",
          "kind": "patch",
          "published_at": "2026-03-28T14:33:36Z"
        },
        {
          "tag": "1.0.0",
          "kind": "major",
          "published_at": "2026-03-20T13:45:39Z"
        },
        {
          "tag": "0.3.12",
          "kind": "patch",
          "published_at": "2026-03-19T05:44:07Z"
        },
        {
          "tag": "0.3.11",
          "kind": "patch",
          "published_at": "2026-03-19T04:37:21Z"
        },
        {
          "tag": "0.3.10",
          "kind": "patch",
          "published_at": "2026-03-16T07:27:05Z"
        },
        {
          "tag": "0.3.9",
          "kind": "patch",
          "published_at": "2026-03-15T05:23:35Z"
        },
        {
          "tag": "0.3.8",
          "kind": "patch",
          "published_at": "2026-03-07T14:06:29Z"
        },
        {
          "tag": "0.3.7",
          "kind": "patch",
          "published_at": "2026-02-18T19:27:20Z"
        },
        {
          "tag": "0.3.6",
          "kind": "patch",
          "published_at": "2026-01-31T06:59:16Z"
        },
        {
          "tag": "0.3.5",
          "kind": "patch",
          "published_at": "2026-01-22T17:08:39Z"
        },
        {
          "tag": "0.3.4",
          "kind": "patch",
          "published_at": "2025-12-09T05:05:24Z"
        },
        {
          "tag": "0.3.3",
          "kind": "patch",
          "published_at": "2025-12-07T07:30:34Z"
        },
        {
          "tag": "0.3.2",
          "kind": "patch",
          "published_at": "2025-12-05T06:21:44Z"
        },
        {
          "tag": "0.3.1",
          "kind": "patch",
          "published_at": "2025-11-24T22:36:39Z"
        },
        {
          "tag": "0.3.0",
          "kind": "minor",
          "published_at": "2025-10-29T02:33:57Z"
        },
        {
          "tag": "0.2.0",
          "kind": "minor",
          "published_at": "2025-10-17T22:58:16Z"
        },
        {
          "tag": "0.1.0",
          "kind": "minor",
          "published_at": "2025-10-15T21:42:07Z"
        }
      ],
      "recent_commits": [
        {
          "oid": "86a1550a95a43d83c8252b62ceeda6f9f03c8288",
          "body": null,
          "is_bot": false,
          "headline": "Fix PHPStan on CakePHP 5.4",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-06-28T10:52:50Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0147c985b379d7d99c7b1339c029a980cb6dbba2",
          "body": "actions/cache@v1-v4 declare Node 20, which GitHub now deprecates and\nforce-runs on Node 24. v5 is Node 24 native and removes the warning.",
          "is_bot": false,
          "headline": "CI: bump actions/cache to v5 (Node 24)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-06-28T09:40:30Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b36e54a62a3f5142627bf2403d6ea91c0e2b9bab",
          "body": "Add support for sebastian/diff 9.x",
          "is_bot": false,
          "headline": "Merge pull request #79 from dereuromark/sebastian-diff-9-support",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-06-18T15:33:51Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "c34b4f73b4eb6298e84b6e000fe33a50700df8ad",
          "body": null,
          "is_bot": false,
          "headline": "Add support for sebastian/diff 9.x",
          "author_name": "Mark",
          "author_login": "dereuromark",
          "committed_at": "2026-06-18T15:28:48Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ae6142d27b6014d07e5fa8aab6e2f5ddebf648c5",
          "body": null,
          "is_bot": false,
          "headline": "Fix PHPCS issues after sniffer update (#78)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-27T16:24:53Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "de23bb35a08daac4199c7af7c0ef1622941aaae2",
          "body": null,
          "is_bot": false,
          "headline": "Restore hidden checkbox field in revert preview",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-26T23:58:54Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0c0df8d5f33b78d296eeea7bf88391e5902649d6",
          "body": null,
          "is_bot": false,
          "headline": "Add sort direction indicators to admin tables",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-26T21:13:38Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "e2724d50caf63831317d64be186454e4500c7e00",
          "body": "* Fix N^2 audit duplication on bulk deleteMany\n\nCakePHP shares a single options object (and therefore one `_auditQueue`)\nacross every entity of a `deleteMany()`, then dispatches\nModel.afterDeleteCommit once per entity. afterCommit() flushed the whole\nshared queue on every dispatch but never cleared \n[…]\nr parent under the same transaction id. Single saves and the afterSave\nstrategy are unaffected.\n\nCovered by integration tests for bulk inserts, audited associations, and\nrollback (no phantom records).",
          "is_bot": false,
          "headline": "Fix saveMany() logging no audit records (#77)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-25T23:35:42Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "3e8e6d6173e9141616a051c82f0450a5d5e2c6cf",
          "body": null,
          "is_bot": false,
          "headline": "Fix audit-stash static analysis follow-up (#74)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-25T14:23:52Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "86b9003bb371d5e71e83037eabdae9f4f31c1471",
          "body": "* Apply conservative Rector cleanup\n\n* Run PHPCS cleanup\n\n* Preserve explicit ForbiddenException handling\n\n* Restore safe NDJSON encoding\n\n* Restore compact() for matching view vars\n\n* Fix operator precedence review issues\n\n* Restore compact() for matching view vars\n\n* Fix cleanup branch static anal\n[…]\nestore parent setUp call\n\n* Restore compact in audit-stash\n\n* Fix audit-stash review follow-ups\n\n* Restore entity property access\n\n* Fix inline var annotation spacing\n\n* Use typed guard for delete log",
          "is_bot": false,
          "headline": "Apply conservative Rector cleanup (#73)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-25T14:10:07Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "0b4fb3c0416d7c89a06b435e8fe569b8b93e6e8a",
          "body": "Point readers to https://toolbox.dereuromark.de/dbml so they can convert this\nDBML snapshot into the migration or raw SQL they need for their own app.",
          "is_bot": false,
          "headline": "Link the DBML converter in the schema snapshot",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-23T15:53:59Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "594a7c4d5acb9ebc882cac311200f675b7339633",
          "body": "Hand-maintained DBML snapshot of the plugin's migration schema under\nresources/schema/schema.dbml — paste into dbdiagram.io to view/diagram.\nMirrors the snapshot already shipped by cakephp-workflow.",
          "is_bot": false,
          "headline": "Add DBML database schema snapshot",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-23T12:53:55Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "593f70827fa53aa544ddbc9f14fe10e6125bc937",
          "body": "* Make audit_logs.primary_key type configurable via Polymorphic.type\n\n* Address review: document Polymorphic.type as a config array and clarify it affects fresh installs",
          "is_bot": false,
          "headline": "Make audit_logs.primary_key type configurable via Polymorphic.type (#71)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-22T23:04:23Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "853bcaccf7120aed96d35b7df1ac581f28c87ec5",
          "body": null,
          "is_bot": false,
          "headline": "Move CONTRIBUTING.md to .github/ and refresh",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-22T18:51:05Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "25aa29ade6b8ad8c7073d002e5c221991976be15",
          "body": "…ary_keys (#70)\n\n* Align audit_logs primary_key signedness with Migrations.unsigned_primary_keys\n\nThe primary_key column stores the polymorphic reference to the audited record and was hardcoded as unsigned.\n\nColumns referencing primary keys must use the same signedness as the application's primary k\n[…]\ngned when unset) so new installs get a matching column. The separate follow-up migration is removed; existing installs are intentionally left untouched. The table's own auto-increment id is unchanged.",
          "is_bot": false,
          "headline": "Align audit_logs primary_key signedness with Migrations.unsigned_prim…",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-22T18:29:23Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a2b3abd74e3da133f81a57f119cceb67afdb4789",
          "body": "- Set cleanUrls so doc pages drop the .html suffix (works on GitHub Pages)\n- Bump deploy-docs actions to the Node 24 majors ahead of the Node 20\n  runner removal (checkout v6, setup-node v6, configure-pages v6,\n  upload-pages-artifact v5, deploy-pages v5)",
          "is_bot": false,
          "headline": "Enable clean docs URLs and update deploy actions to Node 24",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-21T17:20:49Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "e587c257f6d0e658f648eeb9abfc14bb7536b587",
          "body": null,
          "is_bot": false,
          "headline": "Replace date function with CakePHP DateTime (#69)",
          "author_name": "Kevin Pfeifer",
          "author_login": "LordSimal",
          "committed_at": "2026-05-16T15:34:49Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "52b9bccf39efa5ed665babff6fe4513ac836bf98",
          "body": "The behavior hooks per-entity ORM model events\n(Model.beforeSave / afterSave / afterSaveCommit). Anything that doesn't\ngo through Table::save() or Table::delete() — updateAll, deleteAll, the\nquery() builder's update/delete, raw Connection::execute() — is\nsilently un-audited. This is correct CakePHP \n[…]\nparagraph IMPORTANT note to the AuditLogBehavior\nclass-level docblock so it surfaces in IDE tooltips and on any code\nreview of a bulk-write change against an audited table.\n\nNo code changes; doc-only.",
          "is_bot": false,
          "headline": "Document operations that bypass the audit listener (#68)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-12T13:35:51Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "af72db51a94116b9770b482fd6ee47e25346475a",
          "body": "ChainVerifier walked the table in chunks of N rows using\n`WHERE id > $lastId`, with no upper bound. Two concurrent-write\nscenarios produced false-broken reports:\n\n1. A new audit row inserted mid-stream gets its prev_hash from the\n   in-flight last row; if the verifier crosses chunk boundaries while\n\n[…]\n.beforeFind listener to insert a third hash-bearing row between\nthe snapshot query and the chunk walk. With the fix the row is not\nchecked (rowsChecked == 2); without the fix it is (rowsChecked == 3).",
          "is_bot": false,
          "headline": "Snapshot upper id bound at start of chain verification (#67)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-12T13:20:16Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "70a9a591c9bb30e2a8c9c7cbf957d8a811c768c2",
          "body": "The page-header pattern (title + action buttons in\n`d-flex justify-content-between align-items-center`) pushed buttons\noff-viewport on phones because no wrap was allowed. Wrap on viewports\n< sm so buttons fall to a new row instead of being clipped.",
          "is_bot": false,
          "headline": "Make page-header row wrap on phones so action buttons stay on-screen",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-12T08:29:37Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "de44d7f3b73fb88a5d522c04ff2991cb96caa0b1",
          "body": "… (#66)\n\n* Fix related-changes finder for irregular plural and -ies source names\n\n`AuditLogsTable::buildForeignKeyName()` derived the child foreign key\nfrom the parent source by stripping a trailing `s` from the\nsnake-cased name. That produced:\n\n- Categories  -> categorie_id  (should be category_id)\n[…]\nestBuildForeignKeyNameViaPascalCase` continues to pass\nbecause UserProfiles -> user_profiles -> user_profile is correct under\nboth implementations.\n\n* Fix docblock tag order for the extends annotation",
          "is_bot": false,
          "headline": "Fix related-changes finder for irregular plural and -ies source names…",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-11T23:08:13Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a5ebe0a8cb297eeaf4a26cef80d0c383e234323d",
          "body": "The default Bootstrap 5 placeholder color (--bs-secondary-color, ~#757575)\nis dark enough to look like real field values. Override to the gray-500\ntoken so empty fields read clearly as placeholders.",
          "is_bot": false,
          "headline": "Lighten form placeholder color so it's not mistaken for real content",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-11T22:01:56Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "6559e31b9acb6e3893887240f6eb815fd617efc3",
          "body": null,
          "is_bot": false,
          "headline": "Fix CS.",
          "author_name": "Mark",
          "author_login": "dereuromark",
          "committed_at": "2026-05-09T18:46:50Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "fea94b45bce7d04d4d8e12d7249f2ff1db422bf4",
          "body": "Packagist already shows the GitHub repo under Source; the homepage\nfield is more useful pointing at the dedicated docs site.",
          "is_bot": false,
          "headline": "Point composer.json homepage at the VitePress docs site",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-09T13:42:20Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "dbc41b2354a3ce03c02410103c065600017e646d",
          "body": "* Add configurable Back-to-App link to admin header\n\nReads AuditStash.adminBackUrl (any URL — string, array, or anything\n$this->Url->build() accepts). Optional AuditStash.adminBackLabel\noverrides the default \"Back to App\" text.\n\nWhen set, an outline arrow-left button is rendered in the right group\nof the header — left of the Bouncer cross-link if present, before the\nclock. When unset (default), the header looks the same as before.\n\n* Document adminBackUrl + adminBackLabel",
          "is_bot": false,
          "headline": "Add configurable Back-to-App link to admin header (#65)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T12:25:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5771f1cdf15d797fe899d0660f0f4d3c77a419d5",
          "body": "The general .pagination .page-link rule sets color: var(--audit-primary).\nThe active rule then sets background-color: var(--audit-primary) without\noverriding the text color, leaving the current-page number rendered as\npurple text on a purple background — unreadable.\n\nOverride the text color to #fff on the active item.",
          "is_bot": false,
          "headline": "Fix unreadable active page number in pagination (#64)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T11:57:59Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "8ec58cd5bf41a793182ccf5f0a63627c971d4d72",
          "body": null,
          "is_bot": false,
          "headline": "Add CONTRIBUTING.md",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T02:46:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ae658f134d2626d83d081ec5181c44364fa18e9b",
          "body": "Rolling back via VARCHAR(7) silently truncates (MySQL non-strict) or\nhard-rejects (MySQL strict, Postgres) any custom event type longer\nthan 7 chars — exactly the values this migration was added to\nsupport (Audit::log() facade types like 'user.login').\n\nA wider column is forward-compatible, so the safe rollback is to\nleave it at VARCHAR(64) — i.e. an intentional no-op down().",
          "is_bot": false,
          "headline": "Fix WidenTypeColumn::down() — drop the destructive type-column shrink",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T01:40:52Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "99f9a3c6904bcf94cd35100706cded45f22f9f36",
          "body": "…havior",
          "is_bot": false,
          "headline": "Document revert.enabled and revert.auditReverts now that they gate be…",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T01:08:56Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "336904804765fca8b52f0b1f6a6620370b6a8526",
          "body": "Both keys were documented in config/app.example.php but never read by\nany source file — RevertService ran unconditionally and unconditionally\ncreated the revert audit row.\n\n- AuditStash.revert.enabled: when false, revertFull/revertPartial/\n  restoreDeleted throw RuntimeException at the entry point, before any\n  DB work. Defaults to true (the historic behavior).\n- AuditStash.revert.auditReverts: when false, createRevertAudit() bails\n  out without inserting the type=revert row. Defaults to true.",
          "is_bot": false,
          "headline": "Wire revert.enabled and revert.auditReverts (#63)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T01:07:13Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "47bf7b7cbcba7e45a9877a8cbc7d25ad9d65d538",
          "body": "Add two new pages:\n- features/revert.md — Revert/Restore feature (RevertService API, admin\n  routes, audit-of-revert behavior, restore caveats).\n- guide/view-helper.md — AuditHelper API across diff rendering, formatting,\n  badges, tables, and revert/restore action buttons, with a worked example\n  of\n[…]\nalongside beforeLog.\n- guide/testing.md documents Audit::setPersister() for swapping the\n  persister in unit tests.\n\nWire new pages into the guide and features sidebars and the section\noverview pages.",
          "is_bot": false,
          "headline": "Document remaining undocumented features and config",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T01:00:42Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "5d4f51555e69b7bd3a6a0b527e50363948242f67",
          "body": "VitePress treats absolute paths as internal site links and fails\nthe build on dead links. The /src/Persister/TablePersister.php\nreference now points at the GitHub blob URL instead.",
          "is_bot": false,
          "headline": "Fix VitePress build: link TablePersister source via GitHub URL",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T00:47:24Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "c77c97f062f2b01cdbdd0708bcf9bddca22a0eaa",
          "body": null,
          "is_bot": false,
          "headline": "Restore CakePHP version-map paragraph as README intro",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T00:42:26Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "7e96c39294e9d78e1bfc3873fd303482c286db86",
          "body": "- Reduce VitePress hero halo (blur 42px → 72px, opacity 0.55 light /\n  0.45 dark) so the orange shield no longer dominates the landing.\n- Commit docs/package-lock.json (required by deploy-docs workflow\n  cache-dependency-path).\n- Fix stale docs/tamper-evidence.md reference in\n  AuditLogBehaviorTest after move to docs/features/.",
          "is_bot": false,
          "headline": "Polish docs site: soften hero glow, lock npm deps, fix stale path",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T00:41:39Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "73fec29e6d2f4d62bf6738a73c5689147f110a76",
          "body": "Was: today (since midnight) vs full-day yesterday → at 00:01am every\ndashboard showed -100% because today had barely started while yesterday\nwas a full day's activity. Same noise on the active-users tile.\n\nNow: yesterday's window is yesterday-midnight through yesterday at\n`now()`'s H:i:s — apples to apples through the day. At 9am we compare\n9 hours vs 9 hours. The midnight cliff is gone.",
          "is_bot": false,
          "headline": "Compare today vs yesterday up to the SAME time-of-day",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T00:32:42Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "df3451af398f389df3d3b21215a4934395ac79a7",
          "body": "Mirrors the Queue admin layout: brand on the left, cross-link button in\nthe middle (when present), server-time clock on the far right.",
          "is_bot": false,
          "headline": "Reorder header so server clock is the last element",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T00:29:35Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "7e9d306807dbda32e2e46c0011f1189be3f0de1e",
          "body": null,
          "is_bot": false,
          "headline": "Delete .phive/phars.xml",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-04T00:22:35Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "53a3c82ecb92da9a1f04f87b1b53b14f41fcd241",
          "body": null,
          "is_bot": false,
          "headline": "Add CakePHP version map note to README",
          "author_name": "Mark",
          "author_login": null,
          "committed_at": "2026-05-04T00:15:37Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "42ec849d0eee99d620d710c81a6f249a970a6dee",
          "body": "Convert __() / __n() calls in src/ and templates/ to __d('audit_stash', ...) /\n__dn('audit_stash', ...) so user-facing strings live in their own translation\ndomain instead of leaking into the host app's default domain.\n\nAdd resources/locales/audit_stash.pot generated via cake i18n extract so\ntranslators have a stable POT to base language files on.",
          "is_bot": false,
          "headline": "Use plugin-specific i18n domain for translations (#62)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T23:58:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "e7665b972e1560661ed3c24e25b26cd32a1bcd7e",
          "body": "* Add server clock to admin header + debug-only Reset button\n\n- Header: a server-time clock (\\`date('Y-m-d H:i:s')\\`) sits between the\n  brand and the Bouncer cross-link, mirroring the Queue admin header.\n- Sidebar: a \"Debug\" section appears when Configure::read('debug') is\n  truthy with a Reset (tr\n[…]\nng new\n  AuditStashController::reset() action TRUNCATEs audit_logs, refusing\n  to run unless debug is on so a misconfigured route can't wipe\n  production logs.\n\n* phpcs: add missing @throws on reset()",
          "is_bot": false,
          "headline": "Add admin header clock + debug-only Reset (truncate) button (#61)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T23:29:36Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "610793210834b82772b10d904dae57741f33c476",
          "body": "Single source of truth for the index / export filter set. Previously\nthe controller had its own private applyBaseFilters helper plus four\ninlined finder calls, and the export action only ran the base filters\nhalf — so changed_field, field_name+value, and bulk_filter were\nsilently dropped from any ex\n[…]\nhan the preview the user just confirmed.\n\nThe new findForFilters finder is documented as the entry point and\ncalls the existing single-purpose finders internally; their public\ncontracts are unchanged.",
          "is_bot": false,
          "headline": "Move audit-log filter logic into AuditLogsTable::findForFilters",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T21:06:40Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "6b19cbb4c67d87b1122379c7379dd9afb4a7a8e3",
          "body": "The export form silently injected a 30-day created-at floor whenever\ndate_from / date_to were empty. Result: a user filtering the index by\nsource / primary_key / transaction_key sees N rows, clicks Export, and\nthe form reports \"nothing to export\" because the rows are older than 30\ndays. The floor ex\n[…]\nlower\nevery month — when the caller has already pinned the query, it stops\nbeing load-protective and just hides rows the index shows. The hard\ncap remains the safety net for accidentally huge exports.",
          "is_bot": false,
          "headline": "Skip default 30-day export floor when other narrowing filters are set",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T20:54:58Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "e2b60bac59900150269c51ede2e053d9bfa83d85",
          "body": "Match the cakephp-queue posture: the plugin no longer serves any admin\naction unless `AuditStash.adminAccess` is explicitly set to a Closure.\nA missing config key, a non-Closure value, a Closure that returns\nanything other than literal `true`, or a Closure that throws all yield\na 403.\n\nAudit logs co\n[…]\nfor users who want to delegate fully to host AppController\nauth: pass `'adminAccess' => fn() => true`. That's an explicit \"I trust\nthe upstream guard\" choice rather than an accidental forgotten guard.",
          "is_bot": false,
          "headline": "Make AuditStash.adminAccess required (deny-by-default)",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T20:17:42Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "339da30d164eb9917c0d291ed94b7f96b7bc9071",
          "body": "Followup to the plugin-prefix discussion (#60): docs/viewer.md gains\na copy-pasteable callable that resolves both `Plugin.Table` and\n`Table` source forms to the CakePHP-conventional Admin-prefix URL\nwithout needing a per-table mapping. Cross-links to the usage.md\nsection that explains why the source column can be either form.\n\nPure docs, no code change.",
          "is_bot": false,
          "headline": "Document a plugin-aware linkRecord callable",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T20:05:07Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "858c540b4fb5475737963655ba1546eea6476903",
          "body": "The coverage report row for a plugin table (e.g. `Comments.Comments`)\nwas correctly counting events under both the dotted and short alias\n(the AuditLogBehavior persists `getRegistryAlias()`, which can be\neither form depending on how the table was loaded), but the 'View\nactivity' link was always buil\n[…]\nource), and the messy both-aliases-\npopulated state where the dotted form wins and the short-alias rows\nsurface separately as 'empirical' so the operator can decide whether\nto backfill or delete them.",
          "is_bot": false,
          "headline": "Fix coverage report linking the wrong source for plugin tables (#60)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T19:57:51Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "9e87edf0c931d42c2c6ac88a8388ca0eafd5b6f1",
          "body": "When no `filter` query param is set, the controller hands the template\nan empty string for `$filter`. The chip closure compared it directly to\nthe chip key, so the All chip — whose key is the literal string 'all' —\nnever matched and never rendered as active. The page looked like\n\"nothing is selected\" even though All was the implicit current view.\n\nTreat an empty `$filter` as the All state in the closure so the\nimplicit-default and the click-target stay in sync.",
          "is_bot": false,
          "headline": "Highlight the All chip on /admin/audit-stash/coverage",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T19:52:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "2689e52da955b336f39d962d85c9c99bc7893155",
          "body": "A handful of cosmetic + dead-code issues found while sweeping the admin\ntemplates for custom-event handling.\n\nStale entity property references (column rename never propagated):\n- revert_preview.php and restore.php both branched on `->user`, which\n  isn't a column on AuditLog (it was renamed to user_\n[…]\n a CRUD diff.\n\nOptional fields in the email alert (HTML + text):\n- \"Primary Key:\" row is hidden for custom events that aren't scoped\n  to a record, instead of showing an empty value next to the label.",
          "is_bot": false,
          "headline": "Polish admin UI: stale ->user refs, button styles, empty payload copy",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T19:50:13Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "69d4fa579d5e9fbd9f269ae52c37b55b1e836ce9",
          "body": "Followup to the export-page PR. Three nav entry points still pointed\nat the legacy direct-stream URLs and bypassed the new form (with its\nrow-count estimate, format picker, and hard-cap pre-flight):\n\n- templates/element/AuditStash/sidebar.php had separate 'Export CSV'\n  and 'Export JSON' entries hit\n[…]\nrent view would\nexport, just with one extra (worthwhile) click to see the count.\n\nStreaming CSV / JSON / NDJSON download URLs themselves are unchanged\nand still work for direct linking / scripted use.",
          "is_bot": false,
          "headline": "Update sidebar / mobile-nav / index export links to the form page (#59)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T19:43:55Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4e0ca1424abb1ff7dae5e1436b8630de0dfe1ece",
          "body": "…#58)\n\nThe previous export action was an inline 'Export CSV' / 'Export JSON'\nbutton on the index page that hard-coded LIMIT 10000, buffered the\nentire result set with toArray(), and built the response body in\nmemory (php://temp + stream_get_contents for CSV; json_encode the\nfull array for JSON). At \n[…]\nphp gets a new 'export' block documenting the\nthree knobs (hardCap, defaultDays, batchSize). Routes now register\nndjson alongside csv / json extensions.\n\nphpunit: 364 (was 351), phpstan + phpcs clean.",
          "is_bot": false,
          "headline": "Move export to its own page, stream the output, cap result-set size (…",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T19:40:19Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "7293c8b2aca96a7535371e67d7a93c09ad1013eb",
          "body": "Custom events emitted via Audit::log() without a primaryKey land in\naudit_logs with primary_key NULL. The index template still rendered the\nTimeline and Related action buttons for those rows, but both controller\nactions hard-require source AND primary_key and redirect with a flash\nerror otherwise — \n[…]\nrd-scoped rows. Same fix applied to the View Timeline\nbutton on the entry detail page.\n\nThe View action and the index \"New\" badge already handled the null\ncase correctly, so no further changes needed.",
          "is_bot": false,
          "headline": "Hide Timeline / Related buttons for primary-key-less rows",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T19:37:59Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4df000227a0bcf64d9f2f9f71919c68b8b34ff39",
          "body": "* Add native Slack, Teams and Discord notification channels\n\nAdds three platform-native chat channels so users no longer have to\nwrite a custom Channel class to get a readable Slack/Teams/Discord\nmessage — drop in 'class' => SlackChannel::class and the alert renders\nwith the platform's expected card\n[…]\nrnsFalseAndLogsWhenPayloadCannotBeJsonEncoded\n  (uses an invalid UTF-8 payload + a stub PSR-3 logger to assert the\n  channel doesn't issue an HTTP request and emits an \"encoding failed\"\n  log record).",
          "is_bot": false,
          "headline": "Add native Slack and Discord notification channels (#56)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T19:24:12Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "01d75f387528bbbfb067a3b2a60cbc3a1a8bef4f",
          "body": "* Add lifecycle event hooks to AuditMonitor\n\nChannels are the happy-path delivery mechanism, but they are a closed\nset — anything beyond the bundled platforms (Mattermost, PagerDuty,\ncustom incident store, per-context suppression, alert mutation,\nforwarding rule exceptions to Sentry) currently requi\n[…]\nDocs reflect the simpler shape: one paragraph explaining the\nlog-based rule-failure path with a Sentry config snippet, instead of\na fourth event recipe.\n\nphpunit: 345 (was 343), phpstan + phpcs clean.",
          "is_bot": false,
          "headline": "Add lifecycle event hooks to AuditMonitor (#57)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T19:05:29Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "192a58c63ddb70a4413d5639153e01cdd2c4e4c0",
          "body": null,
          "is_bot": false,
          "headline": "Cleanup.",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T18:31:57Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d32d9b9ab695e8c441b311df307af96e1a90d4b1",
          "body": "* Add forensic capture, sensitive-field rule, test assertions trait\n\nThree independent observability additions, all opt-in / additive — no\nexisting call sites change behavior.\n\nEnvironmentMetadata: new `capture` constructor argument lets users\nopt into request-derived meta fields (`user_agent`, `ref\n[…]\nthe\nevent-hook path needs a persister decorator because BaseEvent has no\npublic setter for changed/original. Also points at the existing\n'sensitive' behavior config for the related PII-redaction case.",
          "is_bot": false,
          "headline": "Add forensic capture, sensitive-field rule, test assertions trait (#55)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T18:08:37Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "225bcd33d19023bf51d73144469bad528e2b56fc",
          "body": "* Add admin dashboard and coverage report\n\nThe plugin now ships an at-a-glance dashboard at the Admin root and a\ncoverage report that surfaces which Tables have AuditLog behavior\nattached vs which don't.\n\nDashboard (/admin/audit-stash, configurable via AuditStash.routePath):\n- KPI cards: events toda\n[…]\nash.coverage.hidePlugins` and `AuditStash.coverage.hideTables`\n  documented with default empty arrays + behavior notes.\n\n* Coverage legend: collapsible details with definition list\n\n* Plugin excludes.",
          "is_bot": false,
          "headline": "Add admin dashboard and coverage report (#54)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T17:44:36Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "569d19bde82286cc5ae1ed7466ef6243c6537db5",
          "body": "README + viewer.md: RequestMetadata constructor takes userId / userDisplay,\nnot user. Removed an orphaned 'Compound User Format' section that\ndescribed an id:displayName string trick and a userSeparator config —\nneither exists in src/, both were left over from before the user_id /\nuser_display split\n[…]\n but nothing in src\nactually read it — setting persisterConfig.hashChain silently did\nnothing. Now applied via setConfig() on the lazily-built persister\nwhen the global config is an array. Test added.",
          "is_bot": false,
          "headline": "Fix doc bugs flagged in correctness audit (#53)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T16:19:50Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2c871be2b1df5d304fe432e055103141f0a434a2",
          "body": "* Add custom action event support\n\nLets applications log audit events that don't map to entity CRUD —\n\"user.login\", \"report.exported\", \"permission.granted\", etc. — through\na static facade that flows through the existing persister, hash chain,\nand viewer.\n\nPublic API:\n\n    use AuditStash\\Audit;\n\n    \n[…]\nleft out: VARCHAR(64) is a TablePersister schema\ndetail, not a property of the event, and the Elasticsearch\npersister has no such limit. The DB driver still raises a clear\n'Data too long' on overflow.",
          "is_bot": false,
          "headline": "Add custom action event support (#48)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T16:01:10Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d13d375150f41e6144d70567e5056a5b97e900de",
          "body": null,
          "is_bot": false,
          "headline": "Delete config/Migrations/schema-dump-default.lock",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T15:45:06Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "259e9974491d8499ed1d1d5a2d76b171749ed7b8",
          "body": "PR #46 switched the four Paginator nav links (first/prev/next/last) from\nescape => false to escapeTitle => false. PaginatorHelper only honors the\nescape option — escapeTitle is silently ignored — so the title fell back\nto the default escaped path and the FontAwesome <i> tags rendered as\nliteral HTML\n[…]\nthose four to escape => false. PaginatorHelper does not have\nan escapeTitle option (verified in cakephp/cakephp PaginatorHelper:\nprev/next/first/last/sort all use escape, with no narrower equivalent).",
          "is_bot": false,
          "headline": "Fix pagination icons rendering as raw text in admin (#52)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T15:43:49Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "62afd538631f8ca952fa4e105f1585b91149ead1",
          "body": "When the table's displayField points at a column that resolves to a\nbacked enum, unit enum, or any Stringable value object, casting the\nentity value to string would throw 'Object of class ... could not be\nconverted to string' from afterSave/afterDelete (and the cascade\ndelete branch).\n\nBacked enums \n[…]\n scalar value, unit enums to their\ncase name, Stringable instances via __toString. Anything else that\nisn't scalar yields null so the audit row still gets written instead\nof blowing up the whole save.",
          "is_bot": false,
          "headline": "Convert enum/Stringable display field values for audit log (#51)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-03T15:28:36Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a8fd84c12623c8b5e50a35e49ef7811add694f66",
          "body": "…ttack (#47)\n\nCloses four injection / unsafe-deserialization surfaces in the admin namespace.\n\nLIKE wildcard / DoS via user_id filter\n  AuditLogsController builds a LIKE %value% clause from the user_id query\n  string in three sites (index, applyBaseFilters, export). Without escaping,\n  a value conta\n[…]\nes\n  hook for subclasses that need to whitelist additional types.\n\nTests: six regression cases across AuditLogsControllerTest and\nAuditLogsTableTest; suite remains green at 289 tests / 892 assertions.",
          "is_bot": false,
          "headline": "Harden admin filters against LIKE/JSON/ES injection and unserialize a…",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-05-02T14:04:30Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "0f283e6e937fc758ad88f294897b45f809b02cbc",
          "body": "Using 'escape' => false also disables HTML escaping of attributes (URL, classes, title), not just the title text. The narrower 'escapeTitle' => false keeps attribute escaping on while still allowing the icon HTML in the title.",
          "is_bot": false,
          "headline": "Use escapeTitle instead of escape in pagination element (#46)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-30T23:09:35Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "754bda68f9d7e828a180e65ad0f5312420e35594",
          "body": null,
          "is_bot": false,
          "headline": "Document AuditStash.accessCheck in app.example.php",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-30T15:49:32Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b562574daeeec91ee20e62e59bab46084cada027",
          "body": "Optional gate for the /admin/audit-logs viewer. Set\nAuditStash.accessCheck to a Closure that receives the current request\nand returns literal true to grant access; anything else (non-Closure,\nreturns false, returns a truthy non-bool, throws) yields a 403.\n\nUnset = no-op — the host AppController's au\n[…]\n via Cake\\Log\\Log; ForbiddenException raised\ninside the Closure is respected as-is so callers can short-circuit\nwith their own message.\n\n8 new test cases in AuditLogsControllerTest cover all branches.",
          "is_bot": false,
          "headline": "Add AuditStash.accessCheck Closure gate (opt-in defense-in-depth) (#45)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-30T13:44:12Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c6515d04ab78bac46c253e30b8ad1cf67a5507c1",
          "body": null,
          "is_bot": false,
          "headline": "cs badge.",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-28T15:45:16Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "67e4224b58cea487f9234cb6796557ffdea122b1",
          "body": "* Capture audit event timestamps with microsecond precision\n\nThe BaseEvent constructor formatted the timestamp with DateTime::ATOM,\nwhich truncates at seconds. Persisters that re-parsed the string into\na DateTime therefore lost any sub-second resolution before persistence,\neven on backends (Elastics\n[…]\nmp() is unchanged, and the\nserialized representation remains a string, so existing implementors,\nqueues, and persisters keep working.\n\n* Document opting into sub-second precision on the created column",
          "is_bot": false,
          "headline": "Capture audit event timestamps with microsecond precision (#44)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-27T13:02:18Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "6e8284ee2e42a45f9810db8736b72a81fb2a6685",
          "body": "- Add nonce attribute to 2 inline <script> blocks (layout + revert_preview)\n- Move Form->button 'confirm' option to form-level data-confirm-message\n  on the revert form to avoid the inline onclick that CSP would block\n- Update layout delegate to use form[data-confirm-message] (the\n  postButton-compatible hook) instead of the legacy form[data-confirm]\n\nThis lets the admin UI work under a strict script-src CSP\n(e.g. \"script-src 'self' 'nonce-X'\") without needing\n'unsafe-inline' or 'unsafe-hashes'.",
          "is_bot": false,
          "headline": "Refactor admin UI to be CSP-compatible (#42)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-22T23:51:27Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "dfeefb569f12ca1ed29c2fd9feb5e17d7f49f849",
          "body": "The transaction -> transaction_key change was applied both retroactively\nto the original CreateAuditLogs migration and as a new rename migration.\nThat created a schema divergence: installs created before this change\nran the rename, while fresh installs would have the final column name\nalready and fa\n[…]\nAuditLogs to its original shipped form (transaction\ncolumn + index) so the RenameTransactionToTransactionKey migration is\nthe single source of truth for the rename on both fresh and existing\ninstalls.",
          "is_bot": false,
          "headline": "Revert retroactive schema edit to CreateAuditLogs (#41)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-17T16:35:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "e7ddf6cd2a7c8b7f1ac376bcfc3c3625c4979478",
          "body": "* Add optional SHA-256 hash chain for tamper-evident audit logs\n\nIntroduces an opt-in hash chain over persisted audit rows, giving\nAuditStash the integrity guarantees required for GoBD (DE), SOX (US),\nHIPAA, and similar regulated environments. Disabled by default —\nexisting installs see no change un\n[…]\n- Document hashChain persister option in config/app.example.php\n\n* Fix CS violation in app.example.php\n\nRemove fully qualified class reference to avoid\nSlevomatCodingStandard violation in config file.",
          "is_bot": false,
          "headline": "Optional SHA-256 hash chain for tamper-evident audit logs (#38)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-16T02:50:37Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d915cd2db1a44c5746c475f3e90df67eca700d32",
          "body": "The column name \"transaction\" is a SQL reserved keyword which causes\nsyntax errors on SQLite and can be problematic on other database engines.\nRenaming to \"transaction_key\" avoids the reserved word conflict entirely.\n\nIncludes a migration for existing installations to rename the column.",
          "is_bot": false,
          "headline": "Rename transaction column to transaction_key (#39)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-16T02:44:00Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b1496f4f96808af6a1e441d6d4de46f9eef0a7e5",
          "body": null,
          "is_bot": false,
          "headline": "Fix pre element overflow in standalone layout",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-04-02T14:57:21Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "c8d2d101101d538a1ccdd68bb8e2ee98d44f06d1",
          "body": null,
          "is_bot": false,
          "headline": "Add standalone-ui keyword.",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-28T16:11:20Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "9ef1a726a051054b194947f6e794f65187ea94ad",
          "body": "Sets explicit Bootstrap 5 templates to avoid app templates affecting\nthe standalone admin layout.",
          "is_bot": false,
          "headline": "Add standalone pagination element to prevent style leakage.",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-28T14:11:43Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "62bad139d18cfeb15c05ace529a419fdc63144e9",
          "body": "Extract flash rendering logic into flash/flash.php element for cleaner\nlayout code and easier maintenance.",
          "is_bot": false,
          "headline": "Refactor flash rendering to use dedicated element.",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-28T13:45:39Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d59e0c969fa9cb79d189356b69a47d5105a95225",
          "body": "Use inline flash rendering with consume() to avoid style leakage from\napp layouts and ensure messages are properly consumed from session.",
          "is_bot": false,
          "headline": "Fix flash message rendering in standalone layout.",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-28T13:40:11Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a152684a9c1e471e3a4abec265b7e3a7b93d5b75",
          "body": "Instead of hardcoding 'plugin' => 'AuditStash' and 'prefix' => 'Admin',\nretrieve these values from the current request. This allows the admin\nlayout to work with custom prefixes or no prefix at all.\n\nFixes #36",
          "is_bot": false,
          "headline": "Use dynamic plugin and prefix in admin layout templates (#37)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-22T16:48:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "24d0264bf05dc30380da8a60284cdb4202b12e47",
          "body": "* Add self-contained Bootstrap 5 admin layout and template elements\n\nSimilar to cakephp-queue and cakephp-queue-scheduler, this adds proper own\ntemplating with a self-contained admin interface that doesn't depend on\nthe host application's styling.\n\nChanges:\n- Add templates/layout/audit_stash.php - i\n[…]\nckr initialization\n- Fix date range queries to include full day (00:00:00 to 23:59:59)\n- Update Flatpickr to target .datepicker class\n\n* Disable autocomplete on date inputs to prevent picker conflicts",
          "is_bot": false,
          "headline": "Add self-contained Bootstrap 5 admin layout and template elements (#35)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-19T05:44:25Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "af6ca01fcfcfd3585164ca1bdea08da9dbdde964",
          "body": "Date comparisons now include time component:\n- date_from: appends ' 00:00:00' for start of day\n- date_to: appends ' 23:59:59' for end of day\n\nThis ensures records created during the day are properly included\nin date range queries.",
          "is_bot": false,
          "headline": "Fix date range filtering to include full day",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-19T05:19:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "48591baa25c1ee4960c67b741f44b06f53feb1ff",
          "body": "…template\n\nSame issue as timeline.php - these fields may already be arrays due to\nthe JSON column type configuration in AuditLogsTable.",
          "is_bot": false,
          "headline": "Fix additional json_decode() TypeErrors in RevertService and restore …",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-19T05:06:40Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "dc4aadf21796f259747dae8bf9d1b947dde22fa7",
          "body": "The AuditLogsTable sets 'json' column types for changed, original, and\nmeta fields, so CakePHP automatically decodes them. The template was\ncalling json_decode() on already-decoded arrays, causing TypeErrors.\n\nFixes the issue by checking if the value is a string before decoding.",
          "is_bot": false,
          "headline": "Fix json_decode() TypeError when entity fields are already decoded",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-19T05:03:43Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "6a41404293f6505cd6abf997eb724593aa4515f1",
          "body": "- Update AuditLog entity PHPDoc to reflect reality: these JSON fields\n  can be either string (from database) or array (when set programmatically)\n- Add is_string() checks before json_decode() in all affected locations\n- Fixes test failures caused by previous commit",
          "is_bot": false,
          "headline": "Fix entity types: original/changed/meta can be string or array",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-19T04:51:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "206d312ba104e6b5a1aa393003b89802d7548640",
          "body": "- Remove unused AuditLog imports (FQCN used in annotations instead)\n- Move @var annotations to variable assignment line per convention\n- Use array<\\FQCN> syntax for proper type hints",
          "is_bot": false,
          "headline": "Fix phpcs: use FQCN in @var annotations, move to variable assignment",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-19T04:47:49Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4ce3058f315df4d6b39f71632238783616f8b81b",
          "body": "- Add AuditLog entity import and @var annotations before foreach loops\n  to resolve property access errors on array|EntityInterface union types\n- Remove redundant is_string() check in AuditLogsTable (always true after null check)\n- Add null checks for $log->changed in StateReconstructorService",
          "is_bot": false,
          "headline": "Fix PHPStan errors with entity type annotations",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-19T04:44:39Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "394de8a618c617177bd0bbf85e31908a863e7937",
          "body": "Add both data-toggle (BS4) and data-bs-toggle (BS5) attributes\nto support both Bootstrap versions.",
          "is_bot": false,
          "headline": "Fix collapse toggle for Bootstrap 4 compatibility.",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-19T04:31:09Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "19487d175c0d05f387459ccdee3a8516762b679f",
          "body": "* Add GDPR compliance helpers (anonymize/delete user audit logs)\n\n- Add GdprService with methods: anonymize, delete, export, findByUser, getStats\n- Add GdprCommand (bin/cake audit_stash gdpr) with actions:\n  - stats: View user's audit log statistics\n  - anonymize: Anonymize PII while preserving audi\n[…]\neduce memory usage for users with large audit histories\n- Fix table name casing in docs to match CakePHP registry aliases\n  (PascalCase: Articles, Comments, Users)\n\n* Fix PHPStan errors in GdprService",
          "is_bot": false,
          "headline": "Add GDPR compliance helpers (anonymize/delete user audit logs) (#34)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-18T21:37:25Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "698b6bc88f24a58cfbcf4576af9ce139517b3ad0",
          "body": "…ssions (#33)\n\nFirefox sends \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"\nas Accept header for form submissions. The application/xml part was incorrectly\ntriggering API detection.\n\nNow only considers application/xml as API request when text/html and\napplication/xhtml+xml are NOT present in the Accept header.\n\nFixes #32",
          "is_bot": false,
          "headline": "Fix request source incorrectly detected as API for Firefox form submi…",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-16T07:26:12Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "28aa003a3bb92c19f2f25bff59d3a58181f17fb8",
          "body": null,
          "is_bot": false,
          "headline": "Enable phpcs cache for faster subsequent runs",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-15T15:36:25Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "dd3db2af6ee2edad2085e3b0491dbf1140350e67",
          "body": "The JSON column types cause automatic decoding to arrays.\nExport methods now handle both cases:\n- CSV: encode arrays to JSON strings\n- JSON: use arrays directly or decode strings",
          "is_bot": false,
          "headline": "Fix export methods to handle both array and string JSON values (#31)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-14T03:55:57Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "709573c202d7c5d2f3f615d0a326877fe5b2c90e",
          "body": "* Add CleanupCommand for retention policy enforcement\n\nImplements configurable audit log cleanup with:\n- Default retention period (configurable via AuditStash.retention.default)\n- Per-table retention periods (AuditStash.retention.tables.{name})\n- Command line override with --retention option\n- Table\n[…]\nest for global cleanup with per-table config warning\n\n* Fix --dry-run short flag and retention exception message (#30)\n\n* Initial plan\n\n* Fix --dry-run short flag and exception message trailing period",
          "is_bot": false,
          "headline": "Add CleanupCommand for retention policy enforcement (#25)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T18:31:32Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "feeb3e28224c49de565944c92892962df4787b10",
          "body": "* Add EnvironmentMetadata documentation and clarify retention policies",
          "is_bot": false,
          "headline": "Add EnvironmentMetadata documentation (#29)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T16:16:32Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f4874d61db6922f24e7c89a4894bf4014c9582da",
          "body": "* Add change filtering to ignore insignificant changes\n\nAdds configurable filters to skip logging changes that are not significant:\n- ignoreEmpty: Skip if no changes after filtering (default: true)\n- ignoreTimestampOnly: Skip if only timestamp fields changed (default: false)\n- ignoreFields: Skip if \n[…]\nlast_seen', 'login_count'],\n      'ignoreWhitespace' => true,\n  ]);\n\nThe ChangeFilter class can also be used independently for custom filtering logic.\n\n* Add documentation for change filtering options",
          "is_bot": false,
          "headline": "Add change filtering to ignore insignificant changes (#27)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T16:11:15Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "9b86adf3f42ab691da189f1122075bc2d771e6a5",
          "body": "- Replace bulk_only checkbox with bulk_filter select dropdown\n- Add findNonBulkChanges finder to filter for non-bulk changes\n- Update controller to handle 'yes', 'no', and empty filter values\n- Add test for findNonBulkChanges finder\n\nThis allows users to filter for:\n- All changes (default)\n- Bulk changes only (transactions with 5+ records)\n- Non-bulk changes only (excluding bulk transactions)",
          "is_bot": false,
          "headline": "Change bulk filter from checkbox to select with yes/no/all options (#28)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T16:10:44Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a9e40f53ea6a22f217248af8ffae971405f43b9b",
          "body": "Adds a new metadata collector that enriches audit logs with:\n- request_source: 'web', 'cli', 'api', or custom (e.g., 'queue')\n- Optional extra metadata (server name, deployment, etc.)\n\nAuto-detection:\n- CLI: Detected via PHP_SAPI\n- API: Detected via Accept/Content-Type headers, /api/* URL, or auth h\n[…]\nr::instance()->on(new EnvironmentMetadata());\n  EventManager::instance()->on(new EnvironmentMetadata('queue'));\n  EventManager::instance()->on(new EnvironmentMetadata(null, ['server' => hostname()]));",
          "is_bot": false,
          "headline": "Add EnvironmentMetadata for request source tracking (#26)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T15:56:19Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "23003f4ebdb8459be07a0fab5d1f86d4a357bd58",
          "body": null,
          "is_bot": false,
          "headline": "Make advanced filter layout consistent - help text above inputs",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T15:30:58Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f9797c7eea17acdbf4e0f406b841d914a04647ce",
          "body": null,
          "is_bot": false,
          "headline": "Remove duplicate help text from changed_field filter",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T15:30:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "8500c123500aa2f02fb6b33215dd75fb517251b5",
          "body": "- Move basic filters (source, type, user, dates, etc.) to top\n- Add collapsible \"Advanced Filters\" section for field-level queries\n- Group Field Name + Field Value together with help text clarifying\n  both are required for value search\n- Add descriptive help text for each advanced filter\n- Auto-expand advanced section when those filters are active",
          "is_bot": false,
          "headline": "Improve filter UI with collapsible advanced filters section",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T15:28:51Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b1f52a2ea51962d7cc81d7bcf545a065edd61504",
          "body": "The JSON_CONTAINS_PATH function returns NULL for NULL columns,\nwhich doesn't filter records properly. Explicitly compare to 1\nto ensure only records with the key present are returned.",
          "is_bot": false,
          "headline": "Fix MySQL JSON_CONTAINS_PATH filter returning all records",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T15:26:11Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "31c150ea64236dd7b7a32bbe3f22e597ed51c94b",
          "body": "* Add advanced filtering and querying features for audit log viewer\n\n- Add JsonQueryHelper for DB-agnostic JSON queries (MySQL/PostgreSQL)\n- Add custom finders: findByChangedField, findByChangedFieldValue,\n  findRelatedChanges, findBulkChanges, findBulkChangeStats\n- Update index with filters for cha\n[…]\nneed to bind\n  the comparison value with the correct type (integer vs string)\n- Simplify bulk stats test assertions to check key existence\n  rather than values, since MAX() return type handling varies",
          "is_bot": false,
          "headline": "Add advanced filtering and querying features for audit log viewer (#24)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T15:23:14Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "ab45aa6380f7b914e9169f50ab9a9cd21ceae81c",
          "body": "The Table schema defines JSON columns which CakePHP auto-decodes to arrays.\nHowever, the helper methods expected raw JSON strings.\n\nUpdated methods to accept `array|string|null`:\n- changeSummary()\n- diff()\n- diffInline()\n- metadata()\n- fieldValuesTable()\n\nFixes #22",
          "is_bot": false,
          "headline": "Fix AuditHelper methods to accept both array and string (#23)",
          "author_name": "Mark Scherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T15:06:30Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4dee54caf56c409f823ae783c84d004abe806118",
          "body": null,
          "is_bot": false,
          "headline": "Improve composer.json metadata",
          "author_name": "mscherer",
          "author_login": "dereuromark",
          "committed_at": "2026-03-13T14:51:21Z",
          "body_truncated": false,
          "is_coding_agent": false
        }
      ],
      "releases_count": 26,
      "commits_last_year": 194,
      "latest_release_at": "2026-06-18T15:47:13Z",
      "latest_release_tag": "2.1.0",
      "releases_from_tags": false,
      "days_since_last_push": 29,
      "active_weeks_last_year": 27,
      "days_since_latest_release": 39,
      "mean_days_between_releases": 9.1
    },
    "community": {
      "has_readme": true,
      "has_license": true,
      "has_description": true,
      "has_contributing": true,
      "health_percentage": 57,
      "has_issue_template": false,
      "has_code_of_conduct": false,
      "has_pull_request_template": false
    },
    "ecosystem": {
      "packages": [
        {
          "name": "dereuromark/cakephp-audit-stash",
          "exists": true,
          "license": "MIT",
          "keywords": [
            "logging",
            "plugin",
            "cakephp",
            "Audit",
            "admin-panel",
            "tracking",
            "history",
            "standalone-ui"
          ],
          "ecosystem": "packagist",
          "matches_repo": true,
          "registry_url": "https://packagist.org/packages/dereuromark/cakephp-audit-stash",
          "is_deprecated": false,
          "latest_version": "2.1.0",
          "repository_url": "https://github.com/dereuromark/cakephp-audit-stash",
          "versions_count": 26,
          "total_downloads": 28731,
          "dependents_count": 4,
          "deprecation_note": null,
          "maintainers_count": null,
          "monthly_downloads": 6814,
          "first_published_at": null,
          "latest_published_at": "2026-06-18T15:33:51Z",
          "latest_version_yanked": null,
          "days_since_latest_publish": 39
        }
      ]
    },
    "popularity": {
      "forks": 2,
      "stars": 8,
      "watchers": 1,
      "fork_history": {
        "days": [
          {
            "date": "2026-04-17",
            "count": 1
          },
          {
            "date": "2026-05-16",
            "count": 1
          }
        ],
        "complete": true,
        "collected": 2,
        "total_forks": 2
      },
      "star_history": null,
      "open_issues_and_prs": 1
    },
    "ai_readiness": {
      "has_nix": false,
      "example_dirs": [],
      "has_llms_txt": false,
      "has_dockerfile": false,
      "has_mcp_signal": false,
      "bootstrap_files": [],
      "api_schema_files": [],
      "has_devcontainer": false,
      "typecheck_configs": [],
      "toolchain_manifests": [],
      "largest_source_bytes": 40814,
      "source_files_sampled": 157,
      "oversized_source_files": 0,
      "agent_instruction_files": [],
      "agent_instruction_max_bytes": null
    },
    "dependencies": {
      "manifests": [
        "composer.json",
        "docs/package.json"
      ],
      "advisories": {
        "error": null,
        "scope": null,
        "source": null,
        "findings": [],
        "collected": false,
        "malicious": [],
        "truncated": false,
        "by_severity": {},
        "advisory_count": 0,
        "affected_count": 0,
        "assessed_count": 0,
        "malicious_count": 0,
        "assessed_package": null,
        "unassessed_count": 0,
        "direct_affected_count": 0
      },
      "ecosystems": [
        "npm",
        "packagist"
      ],
      "dependencies": [
        {
          "name": "cakephp/orm",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^5.3.0"
        },
        {
          "name": "sebastian/diff",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^6.0 || ^7.0 || ^8.0 || ^9.0"
        }
      ],
      "all_dependencies": {
        "error": "GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository",
        "source": null,
        "packages": [],
        "collected": false,
        "truncated": false,
        "total_count": null,
        "direct_count": null,
        "indirect_count": null
      }
    },
    "maintainership": {
      "issues": {
        "open_prs": 0,
        "merged_prs": 65,
        "open_issues": 1,
        "closed_ratio": 0.917,
        "closed_issues": 11,
        "closed_unmerged_prs": 3
      },
      "bus_factor": 1,
      "bot_contributors": 0,
      "top_contributors": [
        {
          "type": "User",
          "login": "dereuromark",
          "commits": 194,
          "avatar_url": "https://avatars.githubusercontent.com/u/39854?v=4"
        },
        {
          "type": "User",
          "login": "lorenzo",
          "commits": 107,
          "avatar_url": "https://avatars.githubusercontent.com/u/37621?v=4"
        },
        {
          "type": "User",
          "login": "cnizzardini",
          "commits": 11,
          "avatar_url": "https://avatars.githubusercontent.com/u/171294?v=4"
        },
        {
          "type": "User",
          "login": "jippi",
          "commits": 9,
          "avatar_url": "https://avatars.githubusercontent.com/u/22841?v=4"
        },
        {
          "type": "User",
          "login": "nicosp",
          "commits": 8,
          "avatar_url": "https://avatars.githubusercontent.com/u/4110066?v=4"
        },
        {
          "type": "User",
          "login": "bravo-kernel",
          "commits": 6,
          "avatar_url": "https://avatars.githubusercontent.com/u/230500?v=4"
        },
        {
          "type": "User",
          "login": "swiffer",
          "commits": 6,
          "avatar_url": "https://avatars.githubusercontent.com/u/2990373?v=4"
        },
        {
          "type": "User",
          "login": "amayer5125",
          "commits": 3,
          "avatar_url": "https://avatars.githubusercontent.com/u/3212673?v=4"
        },
        {
          "type": "User",
          "login": "aceat64",
          "commits": 2,
          "avatar_url": "https://avatars.githubusercontent.com/u/348675?v=4"
        },
        {
          "type": "User",
          "login": "blamh",
          "commits": 1,
          "avatar_url": "https://avatars.githubusercontent.com/u/114339?v=4"
        }
      ],
      "contributors_sampled": 18,
      "top_contributor_share": 0.546
    },
    "quality_signals": {
      "has_ci": true,
      "has_tests": true,
      "ci_workflows": [
        "ci.yml",
        "deploy-docs.yml"
      ],
      "has_docs_dir": true,
      "linter_configs": [
        "phpcs.xml",
        "phpstan.neon"
      ],
      "has_editorconfig": true,
      "has_linter_config": true,
      "has_precommit_config": false
    },
    "security_signals": {
      "lockfiles": [
        "package-lock.json"
      ],
      "scorecard": {
        "checks": [
          {
            "name": "Binary-Artifacts",
            "score": 10,
            "reason": "no binaries found in the repo",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#binary-artifacts"
          },
          {
            "name": "Branch-Protection",
            "score": 3,
            "reason": "branch protection is not maximal on development and all release branches",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#branch-protection"
          },
          {
            "name": "CI-Tests",
            "score": 10,
            "reason": "15 out of 15 merged PRs checked by a CI test -- score normalized to 10",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#ci-tests"
          },
          {
            "name": "CII-Best-Practices",
            "score": 0,
            "reason": "no effort to earn an OpenSSF best practices badge detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#cii-best-practices"
          },
          {
            "name": "Code-Review",
            "score": 0,
            "reason": "Found 2/29 approved changesets -- score normalized to 0",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#code-review"
          },
          {
            "name": "Contributors",
            "score": 10,
            "reason": "project has 11 contributing companies or organizations",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#contributors"
          },
          {
            "name": "Dangerous-Workflow",
            "score": 10,
            "reason": "no dangerous workflow patterns detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dangerous-workflow"
          },
          {
            "name": "Dependency-Update-Tool",
            "score": 0,
            "reason": "no update tool detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dependency-update-tool"
          },
          {
            "name": "Fuzzing",
            "score": 0,
            "reason": "project is not fuzzed",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#fuzzing"
          },
          {
            "name": "License",
            "score": 10,
            "reason": "license file detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#license"
          },
          {
            "name": "Maintained",
            "score": 10,
            "reason": "30 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 10",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#maintained"
          },
          {
            "name": "Packaging",
            "score": null,
            "reason": "packaging workflow not detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#packaging"
          },
          {
            "name": "Pinned-Dependencies",
            "score": 1,
            "reason": "dependency not pinned by hash detected -- score normalized to 1",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#pinned-dependencies"
          },
          {
            "name": "SAST",
            "score": 0,
            "reason": "SAST tool is not run on all commits -- score normalized to 0",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#sast"
          },
          {
            "name": "Security-Policy",
            "score": 0,
            "reason": "security policy file not detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#security-policy"
          },
          {
            "name": "Signed-Releases",
            "score": null,
            "reason": "no releases found",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#signed-releases"
          },
          {
            "name": "Token-Permissions",
            "score": 0,
            "reason": "detected GitHub workflow tokens with excessive permissions",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#token-permissions"
          },
          {
            "name": "Vulnerabilities",
            "score": 5,
            "reason": "5 existing vulnerabilities detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
          }
        ],
        "commit": "86a1550a95a43d83c8252b62ceeda6f9f03c8288",
        "ran_at": "2026-07-28T09:55:23Z",
        "aggregate_score": 4.2,
        "scorecard_version": "v5.5.0"
      },
      "has_codeql_workflow": false,
      "has_security_policy": false,
      "has_dependabot_config": false
    },
    "contribution_flow": {
      "collected": true,
      "ci_last_run_at": "2026-06-28T10:53:56Z",
      "oldest_open_prs": [],
      "last_merged_pr_at": "2026-06-28T10:52:51Z",
      "ci_last_conclusion": "SUCCESS",
      "oldest_open_issues": [
        {
          "number": 8,
          "created_at": "2025-12-04T17:31:28Z",
          "last_comment_at": null,
          "last_comment_author": null
        }
      ]
    }
  },
  "config": {
    "disabled_metrics": [],
    "disabled_categories": [],
    "disabled_components": {}
  },
  "source": {
    "url": "https://github.com/dereuromark/cakephp-audit-stash",
    "host": "github.com",
    "name": "cakephp-audit-stash",
    "owner": "dereuromark"
  },
  "metrics": {
    "overall": {
      "key": "overall",
      "band": "moderate",
      "name": "Overall health",
      "note": null,
      "notes": [],
      "value": 69,
      "inputs": {
        "security": 42,
        "vitality": 86,
        "community": 44,
        "governance": 70,
        "engineering": 94
      },
      "components": []
    },
    "categories": [
      {
        "key": "vitality",
        "band": "excellent",
        "name": "Vitality",
        "value": 86,
        "weight": 0.22,
        "metrics": [
          {
            "key": "development_activity",
            "band": "good",
            "name": "Development activity",
            "note": null,
            "notes": [],
            "value": 76,
            "inputs": {
              "commits_last_year": 194,
              "human_commit_share": 1,
              "days_since_last_push": 29,
              "active_weeks_last_year": 27
            },
            "components": [
              {
                "key": "push_recency",
                "name": "Push recency",
                "detail": "last push 29 days ago",
                "points": 28.8,
                "status": "partial",
                "details": [
                  {
                    "code": "push_recency",
                    "params": {
                      "days": 29
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_cadence",
                "name": "Commit cadence",
                "detail": "27/52 weeks with commits",
                "points": 18.7,
                "status": "partial",
                "details": [
                  {
                    "code": "commit_cadence_weeks",
                    "params": {
                      "weeks": 27
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_volume",
                "name": "Commit volume",
                "detail": "194 commits in the last year",
                "points": 18,
                "status": "met",
                "details": [
                  {
                    "code": "commits_last_year",
                    "params": {
                      "count": 194
                    }
                  }
                ],
                "max_points": 18
              },
              {
                "key": "openssf_scorecard_maintained",
                "name": "OpenSSF Scorecard: Maintained",
                "detail": "30 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 10",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "release_discipline",
            "band": "excellent",
            "name": "Release discipline",
            "note": "Excluded from scoring (no data or not applicable): OpenSSF Scorecard: Signed-Releases. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "openssf_scorecard_signed_releases"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 100,
            "inputs": {
              "releases_count": 26,
              "latest_release_tag": "2.1.0",
              "releases_from_tags": false,
              "days_since_latest_release": 39,
              "mean_days_between_releases": 9.1
            },
            "components": [
              {
                "key": "ships_releases",
                "name": "Ships releases",
                "detail": "26 releases published",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "releases_published",
                    "params": {
                      "count": 26
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "release_recency",
                "name": "Release recency",
                "detail": "latest release 39 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "release_recency",
                    "params": {
                      "days": 39
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "release_cadence",
                "name": "Release cadence",
                "detail": "a release every ~9.1 days",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "release_cadence",
                    "params": {
                      "gap": 9.1
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "openssf_scorecard_signed_releases",
                "name": "OpenSSF Scorecard: Signed-Releases",
                "detail": "no releases found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "max_points": 10
              }
            ]
          },
          {
            "key": "abandonment",
            "band": "excellent",
            "name": "Abandonment",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "cap": null,
              "state": "maintained",
              "guards": [],
              "signals": [],
              "red_flag": false,
              "multiplier_pct": 100,
              "declared_reason": null,
              "unverified_reason": null,
              "unanswered_open_prs": null,
              "unanswered_open_issues": null,
              "days_since_last_merged_pr": null,
              "days_since_last_human_commit": 29,
              "days_since_last_human_commit_is_floor": false
            },
            "components": [
              {
                "key": "project_is_still_maintained",
                "name": "Project is still maintained",
                "detail": "last human commit 29 days ago",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "abandonment_maintained",
                    "params": {
                      "days": 29
                    }
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Is the project alive — is code being written and are releases shipping?"
      },
      {
        "key": "community",
        "band": "at_risk",
        "name": "Community & Adoption",
        "value": 44,
        "weight": 0.18,
        "metrics": [
          {
            "key": "popularity",
            "band": "critical",
            "name": "Popularity & adoption",
            "note": null,
            "notes": [],
            "value": 14,
            "inputs": {
              "forks": 2,
              "stars": 8,
              "watchers": 1,
              "growth_state": "unverified",
              "growth_factor_pct": 100,
              "growth_unverified_reason": "no_history"
            },
            "components": [
              {
                "key": "stars",
                "name": "Stars",
                "detail": "8 stars",
                "points": 13.7,
                "status": "partial",
                "details": [
                  {
                    "code": "stars",
                    "params": {
                      "count": 8
                    }
                  }
                ],
                "max_points": 60
              },
              {
                "key": "forks",
                "name": "Forks",
                "detail": "2 forks",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "forks",
                    "params": {
                      "count": 2
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "watchers",
                "name": "Watchers",
                "detail": "1 watchers",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "watchers",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 15
              }
            ]
          },
          {
            "key": "community_health",
            "band": "good",
            "name": "Community health",
            "note": null,
            "notes": [],
            "value": 70,
            "inputs": {
              "has_readme": true,
              "has_license": true,
              "has_contributing": true,
              "has_issue_template": false,
              "has_code_of_conduct": false,
              "has_pull_request_template": false
            },
            "components": [
              {
                "key": "readme",
                "name": "README",
                "detail": null,
                "points": 22.5,
                "status": "met",
                "details": [],
                "max_points": 22.5
              },
              {
                "key": "license",
                "name": "License",
                "detail": "recognized license (MIT)",
                "points": 22.5,
                "status": "met",
                "details": [
                  {
                    "code": "license_standard",
                    "params": {}
                  },
                  {
                    "code": "license_spdx",
                    "params": {
                      "spdx": "MIT"
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributing_guide",
                "name": "CONTRIBUTING guide",
                "detail": null,
                "points": 18,
                "status": "met",
                "details": [],
                "max_points": 18
              },
              {
                "key": "code_of_conduct",
                "name": "Code of conduct",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 13.5
              },
              {
                "key": "issue_template",
                "name": "Issue template",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.2
              },
              {
                "key": "pr_template",
                "name": "PR template",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 6.3
              }
            ]
          },
          {
            "key": "ecosystem_adoption",
            "band": "moderate",
            "name": "Ecosystem adoption (downloads)",
            "note": null,
            "notes": [],
            "value": 56,
            "inputs": {
              "packages": [
                "dereuromark/cakephp-audit-stash"
              ],
              "dependents": 4,
              "ecosystems": "packagist",
              "total_downloads": 28731,
              "monthly_downloads": 6814
            },
            "components": [
              {
                "key": "monthly_downloads",
                "name": "Monthly downloads",
                "detail": "6,814 downloads/month across packagist",
                "points": 51.1,
                "status": "partial",
                "details": [
                  {
                    "code": "downloads_monthly",
                    "params": {
                      "count": 6814,
                      "ecosystems": "packagist"
                    }
                  }
                ],
                "max_points": 80
              },
              {
                "key": "registry_dependents",
                "name": "Registry dependents",
                "detail": "4 packages depend on it",
                "points": 4.7,
                "status": "partial",
                "details": [
                  {
                    "code": "registry_dependents",
                    "params": {
                      "count": 4
                    }
                  }
                ],
                "max_points": 20
              }
            ]
          }
        ],
        "description": "Does the project have users, downloads, attention, and a welcoming setup for contributors?"
      },
      {
        "key": "governance",
        "band": "good",
        "name": "Sustainability & Governance",
        "value": 70,
        "weight": 0.24,
        "metrics": [
          {
            "key": "maintainer_resilience",
            "band": "at_risk",
            "name": "Maintainer resilience (bus factor)",
            "note": null,
            "notes": [],
            "value": 43,
            "inputs": {
              "bus_factor": 1,
              "contributors_sampled": 18,
              "top_contributor_share": 0.546
            },
            "components": [
              {
                "key": "bus_factor",
                "name": "Bus factor",
                "detail": "1 contributor(s) cover half of all commits",
                "points": 9,
                "status": "partial",
                "details": [
                  {
                    "code": "bus_factor",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 54
              },
              {
                "key": "commit_distribution",
                "name": "Commit distribution",
                "detail": "top contributor authored 55% of commits",
                "points": 10.2,
                "status": "partial",
                "details": [
                  {
                    "code": "top_contributor_share",
                    "params": {
                      "share": 55
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributor_breadth",
                "name": "Contributor breadth",
                "detail": "18 contributors",
                "points": 13.5,
                "status": "met",
                "details": [
                  {
                    "code": "contributors_sampled",
                    "params": {
                      "count": 18
                    }
                  }
                ],
                "max_points": 13.5
              },
              {
                "key": "openssf_scorecard_contributors",
                "name": "OpenSSF Scorecard: Contributors",
                "detail": "project has 11 contributing companies or organizations",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "responsiveness",
            "band": "good",
            "name": "Issue & PR responsiveness",
            "note": null,
            "notes": [],
            "value": 80,
            "inputs": {
              "merged_prs": 65,
              "open_issues": 1,
              "closed_issues": 11,
              "issue_closed_ratio": 0.917,
              "closed_unmerged_prs": 3
            },
            "components": [
              {
                "key": "issue_resolution",
                "name": "Issue resolution",
                "detail": "92% of issues closed",
                "points": 42.9,
                "status": "partial",
                "details": [
                  {
                    "code": "issues_closed_share",
                    "params": {
                      "share": 92
                    }
                  }
                ],
                "max_points": 46.75
              },
              {
                "key": "pr_acceptance",
                "name": "PR acceptance",
                "detail": "65/68 decided PRs merged",
                "points": 36.6,
                "status": "partial",
                "details": [
                  {
                    "code": "decided_prs_merged",
                    "params": {
                      "merged": 65,
                      "decided": 68
                    }
                  }
                ],
                "max_points": 38.25
              },
              {
                "key": "openssf_scorecard_code_review",
                "name": "OpenSSF Scorecard: Code-Review",
                "detail": "Found 2/29 approved changesets -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 15
              }
            ]
          },
          {
            "key": "stewardship",
            "band": "good",
            "name": "Ownership & stewardship",
            "note": "Excluded from scoring (no data or not applicable): Verified domain. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "verified_domain"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 70,
            "inputs": {
              "followers": 750,
              "owner_type": "User",
              "is_verified": null,
              "owner_login": "dereuromark",
              "public_repos": 123,
              "account_age_days": 6437
            },
            "components": [
              {
                "key": "ownership_backing",
                "name": "Ownership backing",
                "detail": "personal (user) account",
                "points": 10,
                "status": "partial",
                "details": [
                  {
                    "code": "owner_personal",
                    "params": {}
                  }
                ],
                "max_points": 30
              },
              {
                "key": "verified_domain",
                "name": "Verified domain",
                "detail": "not applicable to user accounts",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "not_applicable_to_user_accounts",
                    "params": {}
                  }
                ],
                "max_points": 20
              },
              {
                "key": "owner_reach",
                "name": "Owner reach",
                "detail": "750 followers of dereuromark",
                "points": 20.7,
                "status": "partial",
                "details": [
                  {
                    "code": "owner_followers",
                    "params": {
                      "count": 750,
                      "login": "dereuromark"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "track_record",
                "name": "Track record",
                "detail": "123 public repos, account ~17 yr old",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "public_repos",
                    "params": {
                      "count": 123
                    }
                  },
                  {
                    "code": "account_age_years",
                    "params": {
                      "years": 17
                    }
                  }
                ],
                "max_points": 25
              }
            ]
          },
          {
            "key": "package_maintenance",
            "band": "excellent",
            "name": "Package maintenance",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "packages": [
                "dereuromark/cakephp-audit-stash"
              ],
              "ecosystems": "packagist",
              "any_deprecated": false,
              "min_days_since_publish": 39
            },
            "components": [
              {
                "key": "published_resolvable",
                "name": "Published & resolvable",
                "detail": "1 package(s) on packagist",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "packages_published",
                    "params": {
                      "count": 1,
                      "ecosystems": "packagist"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "publish_recency",
                "name": "Publish recency",
                "detail": "latest publish 39 days ago",
                "points": 35,
                "status": "met",
                "details": [
                  {
                    "code": "publish_recency",
                    "params": {
                      "days": 39
                    }
                  }
                ],
                "max_points": 35
              },
              {
                "key": "version_history",
                "name": "Version history",
                "detail": "26 published versions",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "published_versions",
                    "params": {
                      "count": 26
                    }
                  }
                ],
                "max_points": 20
              },
              {
                "key": "not_deprecated",
                "name": "Not deprecated",
                "detail": "active, not deprecated or yanked",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "package_not_deprecated",
                    "params": {}
                  }
                ],
                "max_points": 20
              }
            ]
          }
        ],
        "description": "Will the project survive its people — bus factor, responsiveness, who backs it, and package upkeep?"
      },
      {
        "key": "engineering",
        "band": "excellent",
        "name": "Engineering Quality",
        "value": 94,
        "weight": 0.2,
        "metrics": [
          {
            "key": "engineering_practices",
            "band": "excellent",
            "name": "Engineering practices",
            "note": null,
            "notes": [],
            "value": 90,
            "inputs": {
              "has_ci": true,
              "has_tests": true,
              "has_editorconfig": true,
              "has_linter_config": true,
              "has_precommit_config": false
            },
            "components": [
              {
                "key": "ci_workflows",
                "name": "CI workflows",
                "detail": "2 workflow(s)",
                "points": 24,
                "status": "met",
                "details": [
                  {
                    "code": "ci_workflows",
                    "params": {
                      "count": 2
                    }
                  }
                ],
                "max_points": 24
              },
              {
                "key": "tests_present",
                "name": "Tests present",
                "detail": null,
                "points": 24,
                "status": "met",
                "details": [],
                "max_points": 24
              },
              {
                "key": "linter_config",
                "name": "Linter config",
                "detail": "phpcs.xml, phpstan.neon",
                "points": 16,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "phpcs.xml, phpstan.neon"
                    }
                  }
                ],
                "max_points": 16
              },
              {
                "key": "pre_commit_hooks",
                "name": "Pre-commit hooks",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 9.6
              },
              {
                "key": "editorconfig",
                "name": ".editorconfig",
                "detail": null,
                "points": 6.4,
                "status": "met",
                "details": [],
                "max_points": 6.4
              },
              {
                "key": "openssf_scorecard_ci_tests",
                "name": "OpenSSF Scorecard: CI-Tests",
                "detail": "15 out of 15 merged PRs checked by a CI test -- score normalized to 10",
                "points": 20,
                "status": "met",
                "details": [],
                "max_points": 20
              }
            ]
          },
          {
            "key": "documentation",
            "band": "excellent",
            "name": "Documentation",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "topics": [
                "audit-log",
                "audit-trail",
                "logging",
                "cakephp",
                "cakephp-plugin",
                "php"
              ],
              "has_wiki": true,
              "homepage": "https://dereuromark.github.io/cakephp-audit-stash/",
              "has_readme": true,
              "has_docs_dir": true,
              "has_description": true
            },
            "components": [
              {
                "key": "readme",
                "name": "README",
                "detail": null,
                "points": 30,
                "status": "met",
                "details": [],
                "max_points": 30
              },
              {
                "key": "documentation_directory",
                "name": "Documentation directory",
                "detail": null,
                "points": 25,
                "status": "met",
                "details": [],
                "max_points": 25
              },
              {
                "key": "documentation_homepage_site",
                "name": "Documentation / homepage site",
                "detail": "https://dereuromark.github.io/cakephp-audit-stash/",
                "points": 15,
                "status": "met",
                "details": [],
                "max_points": 15
              },
              {
                "key": "repository_description",
                "name": "Repository description",
                "detail": null,
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              },
              {
                "key": "topics",
                "name": "Topics",
                "detail": "6 topics",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "topics_count",
                    "params": {
                      "count": 6
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "wiki",
                "name": "Wiki",
                "detail": null,
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          }
        ],
        "description": "Are baseline engineering and documentation practices in place?"
      },
      {
        "key": "security",
        "band": "at_risk",
        "name": "Security",
        "value": 42,
        "weight": 0.16,
        "metrics": [
          {
            "key": "security_posture",
            "band": "at_risk",
            "name": "Security posture",
            "note": "Excluded from scoring (no data or not applicable): Packaging, Signed-Releases. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "packaging",
                    "signed_releases"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 42,
            "inputs": {
              "source": "openssf_scorecard",
              "checks_evaluated": 16,
              "scorecard_version": "v5.5.0",
              "checks_inconclusive": 2,
              "scorecard_aggregate": 4.2
            },
            "components": [
              {
                "key": "binary_artifacts",
                "name": "Binary-Artifacts",
                "detail": "no binaries found in the repo",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "branch_protection",
                "name": "Branch-Protection",
                "detail": "branch protection is not maximal on development and all release branches",
                "points": 2.2,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "ci_tests",
                "name": "CI-Tests",
                "detail": "15 out of 15 merged PRs checked by a CI test -- score normalized to 10",
                "points": 2.5,
                "status": "met",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "cii_best_practices",
                "name": "CII-Best-Practices",
                "detail": "no effort to earn an OpenSSF best practices badge detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "code_review",
                "name": "Code-Review",
                "detail": "Found 2/29 approved changesets -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "contributors",
                "name": "Contributors",
                "detail": "project has 11 contributing companies or organizations",
                "points": 2.5,
                "status": "met",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "dangerous_workflow",
                "name": "Dangerous-Workflow",
                "detail": "no dangerous workflow patterns detected",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              },
              {
                "key": "dependency_update_tool",
                "name": "Dependency-Update-Tool",
                "detail": "no update tool detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "fuzzing",
                "name": "Fuzzing",
                "detail": "project is not fuzzed",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 5
              },
              {
                "key": "license",
                "name": "License",
                "detail": "license file detected",
                "points": 2.5,
                "status": "met",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "maintained",
                "name": "Maintained",
                "detail": "30 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 10",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "packaging",
                "name": "Packaging",
                "detail": "packaging workflow not detected",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "max_points": 5
              },
              {
                "key": "pinned_dependencies",
                "name": "Pinned-Dependencies",
                "detail": "dependency not pinned by hash detected -- score normalized to 1",
                "points": 0.5,
                "status": "partial",
                "details": [],
                "max_points": 5
              },
              {
                "key": "sast",
                "name": "SAST",
                "detail": "SAST tool is not run on all commits -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 5
              },
              {
                "key": "security_policy",
                "name": "Security-Policy",
                "detail": "security policy file not detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 5
              },
              {
                "key": "signed_releases",
                "name": "Signed-Releases",
                "detail": "no releases found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "max_points": 7.5
              },
              {
                "key": "token_permissions",
                "name": "Token-Permissions",
                "detail": "detected GitHub workflow tokens with excessive permissions",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "vulnerabilities",
                "name": "Vulnerabilities",
                "detail": "5 existing vulnerabilities detected",
                "points": 3.8,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              }
            ]
          },
          {
            "key": "high_risk_jurisdiction_exposure",
            "band": "excellent",
            "name": "High-Risk Jurisdiction Exposure",
            "note": "Only high-confidence self-published location evidence affects this multiplier. Ambiguous matches are review-only; country evidence is not proof of nationality, citizenship, legal registration, malicious intent, or sanctions status.",
            "notes": [
              {
                "code": "jurisdiction_evidence_limits",
                "params": {}
              }
            ],
            "value": 100,
            "inputs": {
              "meaning": "self-published location evidence; not nationality or citizenship",
              "red_flag": false,
              "exposures": [],
              "policy_countries": [
                "Russia",
                "Iran",
                "North Korea"
              ],
              "review_only_matches": 0,
              "assessed_self_published_locations": 17
            },
            "components": [
              {
                "key": "policy_exposure_multiplier",
                "name": "Policy exposure multiplier",
                "detail": "no confirmed policy-scope location match",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "jurisdiction_no_match",
                    "params": {}
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Are visible security and supply-chain practices strong, with no malicious dependency and no unresolved high-risk jurisdiction exposure?"
      },
      {
        "key": "ai_readiness",
        "band": "at_risk",
        "name": "AI Readiness",
        "value": 45,
        "weight": 0,
        "metrics": [
          {
            "key": "ai_agent_context",
            "band": "at_risk",
            "name": "Agent context & guidance",
            "note": null,
            "notes": [],
            "value": 40,
            "inputs": {
              "has_llms_txt": false,
              "legible_history_share": 0.79,
              "agent_instruction_files": [],
              "agent_instruction_max_bytes": null
            },
            "components": [
              {
                "key": "agent_instructions",
                "name": "Agent instructions",
                "detail": "no CLAUDE.md / AGENTS.md / editor rules",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_agent_instructions",
                    "params": {}
                  }
                ],
                "max_points": 45
              },
              {
                "key": "machine_readable_docs_llms_txt",
                "name": "Machine-readable docs (llms.txt)",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 15
              },
              {
                "key": "legible_commit_history",
                "name": "Legible commit history",
                "detail": "79 of 100 human commits state their intent (structured subject or explanatory body)",
                "points": 40,
                "status": "met",
                "details": [
                  {
                    "code": "legible_history",
                    "params": {
                      "legible": 79,
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 40
              }
            ]
          },
          {
            "key": "ai_verify_loop",
            "band": "at_risk",
            "name": "Verify loop (build / test / typecheck)",
            "note": null,
            "notes": [],
            "value": 44,
            "inputs": {
              "has_nix": false,
              "has_tests": true,
              "lockfiles": [
                "package-lock.json"
              ],
              "has_dockerfile": false,
              "typed_language": false,
              "bootstrap_files": [],
              "has_devcontainer": false,
              "has_linter_config": true,
              "typecheck_configs": [],
              "agent_commit_share": 0,
              "toolchain_manifests": [],
              "dependency_bot_commit_share": 0
            },
            "components": [
              {
                "key": "one_command_bootstrap",
                "name": "One-command bootstrap",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 18
              },
              {
                "key": "automated_tests",
                "name": "Automated tests",
                "detail": null,
                "points": 22,
                "status": "met",
                "details": [],
                "max_points": 22
              },
              {
                "key": "lint_format_config",
                "name": "Lint / format config",
                "detail": "phpcs.xml, phpstan.neon",
                "points": 11,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "phpcs.xml, phpstan.neon"
                    }
                  }
                ],
                "max_points": 11
              },
              {
                "key": "static_type_checking",
                "name": "Static type checking",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 11
              },
              {
                "key": "reproducible_environment",
                "name": "Reproducible environment",
                "detail": "lockfile",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "lockfile"
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "demonstrated_agent_practice",
                "name": "Demonstrated agent practice",
                "detail": "no agent-authored commits among the last 100",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_agent_authored_commits",
                    "params": {
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "automated_maintenance",
                "name": "Automated maintenance",
                "detail": "no automated dependency updates observed",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_dependency_automation",
                    "params": {}
                  }
                ],
                "max_points": 8
              },
              {
                "key": "openssf_scorecard_pinned_dependencies",
                "name": "OpenSSF Scorecard: Pinned-Dependencies",
                "detail": "dependency not pinned by hash detected -- score normalized to 1",
                "points": 1,
                "status": "partial",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "ai_code_legibility",
            "band": "moderate",
            "name": "Code legibility for models",
            "note": null,
            "notes": [],
            "value": 55,
            "inputs": {
              "primary_language": "PHP",
              "largest_source_bytes": 40814,
              "source_files_sampled": 157,
              "oversized_source_files": 0
            },
            "components": [
              {
                "key": "type_checkable_code",
                "name": "Type-checkable code",
                "detail": "PHP without a type-check config",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_typecheck_config_language",
                    "params": {
                      "language": "PHP"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "manageable_file_sizes",
                "name": "Manageable file sizes",
                "detail": "0/157 source files over 60KB",
                "points": 55,
                "status": "met",
                "details": [
                  {
                    "code": "oversized_source_files",
                    "params": {
                      "kb": 60,
                      "sampled": 157,
                      "oversized": 0
                    }
                  }
                ],
                "max_points": 55
              }
            ]
          }
        ],
        "description": "How well is the repo equipped to be developed and maintained with AI coding agents? An independent, experimental badge — weight 0.0, so it is surfaced on its own and does not affect the overall health score."
      }
    ],
    "metrics_version": "1.13.0"
  },
  "warnings": [
    "Star history unavailable: GitHub GraphQL error: Resource not accessible by personal access token",
    "GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository"
  ],
  "report_type": "repository",
  "generated_at": "2026-07-28T09:55:41.694632Z",
  "schema_version": "0.27.0",
  "badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/d/dereuromark/cakephp-audit-stash.svg",
  "full_name": "dereuromark/cakephp-audit-stash",
  "license_state": "standard",
  "license_spdx": "MIT"
}

评分是信号,而非担保。 评分反映的是 GitHub 上公开可见的实践——不是代码审计,也不是安全保证。

缺失数据将被剔除并重新归一化权重,绝不按零分计。方法论已版本化并公开:指标 v1.13.0、模式 v0.27.0—— 完整方法论 · 指标知识库.

单项结果在整体记录中的位置: 汇总统计Packagist.