@zalom/plastic 2.0.0-alpha.8 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/PLASTIC.md +13 -136
  2. package/README.md +357 -133
  3. package/agents/plastic-enforcer.md +20 -15
  4. package/agents/plastic-executor.md +15 -4
  5. package/agents/plastic-node-research.md +30 -0
  6. package/agents/plastic-node-verify.md +28 -0
  7. package/agents/plastic-node-work.md +33 -0
  8. package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
  9. package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
  10. package/assets/plastic-logo.svg +1 -0
  11. package/bin/crap +4 -0
  12. package/bin/lib/context_budget.rb +43 -6
  13. package/bin/lib/skill_census.rb +839 -0
  14. package/bin/plastic +6 -0
  15. package/bin/plastic-skill-census +114 -0
  16. package/bin/test +24 -4
  17. package/bin/verify-change +345 -0
  18. package/config_asks.yml +4 -4
  19. package/deprecations.yml +1 -1
  20. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
  21. package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
  22. package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
  23. package/docs/help/human-report-contract.md +152 -0
  24. package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
  25. package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +23 -13
  26. package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
  27. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  28. package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
  29. package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
  30. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  31. package/hooks/call-budget +4 -0
  32. package/hooks/hooks.json +24 -0
  33. package/hooks/message-display +62 -1
  34. package/hooks/session-start +5 -1
  35. package/hooks/statusline +32 -27
  36. package/hooks/stop +5 -0
  37. package/package.json +5 -3
  38. package/scripts/append-ledger +2 -1
  39. package/scripts/dashboard.rb +267 -16
  40. package/scripts/day-summary +2 -1
  41. package/scripts/doctor.rb +680 -39
  42. package/scripts/end-intent +238 -22
  43. package/scripts/exec-worktree +5 -5
  44. package/scripts/file-session-intent +2 -1
  45. package/scripts/graph-measure +249 -0
  46. package/scripts/hook-call-budget +222 -0
  47. package/scripts/hook-capture +24 -123
  48. package/scripts/hook-close +2 -1
  49. package/scripts/hook-message-display +19 -5
  50. package/scripts/hook-record +24 -16
  51. package/scripts/hook-savepoint +27 -3
  52. package/scripts/hook-session-start +359 -321
  53. package/scripts/hook-stop +58 -0
  54. package/scripts/index-projection +74 -0
  55. package/scripts/insight-append +17 -4
  56. package/scripts/install.rb +9 -7
  57. package/scripts/lib/action_graph_shim.rb +279 -0
  58. package/scripts/lib/active_delivery.rb +105 -0
  59. package/scripts/lib/agent_models.rb +63 -25
  60. package/scripts/lib/arm.rb +48 -83
  61. package/scripts/lib/atomic_write.rb +31 -0
  62. package/scripts/lib/backup.rb +65 -0
  63. package/scripts/lib/cli/command.rb +92 -0
  64. package/scripts/lib/cli/commands/auto.rb +18 -0
  65. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  66. package/scripts/lib/cli/commands/auto_lock.rb +99 -0
  67. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  68. package/scripts/lib/cli/commands/auto_take.rb +31 -0
  69. package/scripts/lib/cli/commands/backup.rb +43 -0
  70. package/scripts/lib/cli/commands/checkout.rb +25 -0
  71. package/scripts/lib/cli/commands/continue.rb +66 -0
  72. package/scripts/lib/cli/commands/doctor.rb +81 -0
  73. package/scripts/lib/cli/commands/feedback.rb +40 -0
  74. package/scripts/lib/cli/commands/help.rb +69 -0
  75. package/scripts/lib/cli/commands/hook.rb +32 -0
  76. package/scripts/lib/cli/commands/index.rb +23 -0
  77. package/scripts/lib/cli/commands/install.rb +21 -0
  78. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  79. package/scripts/lib/cli/commands/intent.rb +19 -0
  80. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  81. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  82. package/scripts/lib/cli/commands/intent_end.rb +62 -0
  83. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  84. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  85. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  86. package/scripts/lib/cli/commands/intent_show.rb +32 -0
  87. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  88. package/scripts/lib/cli/commands/intent_step.rb +71 -0
  89. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  90. package/scripts/lib/cli/commands/migrate.rb +16 -0
  91. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  92. package/scripts/lib/cli/commands/next.rb +54 -0
  93. package/scripts/lib/cli/commands/project.rb +19 -0
  94. package/scripts/lib/cli/commands/project_links.rb +42 -0
  95. package/scripts/lib/cli/commands/project_list.rb +20 -0
  96. package/scripts/lib/cli/commands/project_new.rb +72 -0
  97. package/scripts/lib/cli/commands/query.rb +31 -0
  98. package/scripts/lib/cli/commands/render.rb +33 -0
  99. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  100. package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
  101. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  102. package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
  103. package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
  104. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  105. package/scripts/lib/cli/commands/rollback.rb +20 -0
  106. package/scripts/lib/cli/commands/search.rb +60 -0
  107. package/scripts/lib/cli/commands/session.rb +18 -0
  108. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  109. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  110. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  111. package/scripts/lib/cli/commands/status.rb +68 -0
  112. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  113. package/scripts/lib/cli/commands/sync.rb +46 -0
  114. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  115. package/scripts/lib/cli/commands/update.rb +20 -0
  116. package/scripts/lib/cli/commands/version.rb +53 -0
  117. package/scripts/lib/cli/frontier.rb +86 -0
  118. package/scripts/lib/cli/intent_progress.rb +36 -0
  119. package/scripts/lib/cli/legacy.rb +80 -0
  120. package/scripts/lib/cli/output.rb +137 -0
  121. package/scripts/lib/cli/scope.rb +134 -0
  122. package/scripts/lib/cli/table.rb +65 -0
  123. package/scripts/lib/cli.rb +94 -0
  124. package/scripts/lib/codex_adapter.rb +198 -0
  125. package/scripts/lib/compact_instructions.rb +13 -5
  126. package/scripts/lib/core_integrity.rb +71 -0
  127. package/scripts/lib/dashboard_screen.rb +40 -0
  128. package/scripts/lib/data_boundary.rb +132 -0
  129. package/scripts/lib/day_summary.rb +30 -16
  130. package/scripts/lib/doctor_core.rb +114 -38
  131. package/scripts/lib/doctor_session_ledger.rb +5 -53
  132. package/scripts/lib/engine_permissions.rb +88 -0
  133. package/scripts/lib/exec_worktree.rb +24 -21
  134. package/scripts/lib/feedback_report.rb +1 -1
  135. package/scripts/lib/graph_edges.rb +137 -0
  136. package/scripts/lib/graph_file.rb +246 -0
  137. package/scripts/lib/graph_measure.rb +645 -0
  138. package/scripts/lib/graph_measure_budget.rb +409 -0
  139. package/scripts/lib/graph_measure_cohorts.rb +487 -0
  140. package/scripts/lib/graph_measure_models.rb +413 -0
  141. package/scripts/lib/graph_measure_report.rb +532 -0
  142. package/scripts/lib/graph_tree.rb +98 -0
  143. package/scripts/lib/guarded_append.rb +155 -0
  144. package/scripts/lib/handoff.rb +40 -13
  145. package/scripts/lib/harness_adapter.rb +184 -0
  146. package/scripts/lib/hook_registry.rb +27 -3
  147. package/scripts/lib/hook_replay.rb +229 -0
  148. package/scripts/lib/index_entry.rb +62 -0
  149. package/scripts/lib/index_projection.rb +201 -0
  150. package/scripts/lib/insights.rb +1 -1
  151. package/scripts/lib/installer_core.rb +532 -71
  152. package/scripts/lib/intent_screen.rb +9 -6
  153. package/scripts/lib/intent_screen_ansi.rb +73 -12
  154. package/scripts/lib/intent_validator.rb +2 -2
  155. package/scripts/lib/lock.rb +10 -11
  156. package/scripts/lib/message_display.rb +377 -173
  157. package/scripts/lib/meter_watch.rb +185 -0
  158. package/scripts/lib/node_file.rb +234 -0
  159. package/scripts/lib/node_ids.rb +99 -0
  160. package/scripts/lib/node_input.rb +913 -0
  161. package/scripts/lib/node_input_compatibility.rb +62 -0
  162. package/scripts/lib/node_ledger.rb +386 -0
  163. package/scripts/lib/node_progress.rb +153 -0
  164. package/scripts/lib/node_return.rb +204 -0
  165. package/scripts/lib/node_worktree.rb +337 -0
  166. package/scripts/lib/outcome_report.rb +440 -0
  167. package/scripts/lib/preflight.rb +4 -6
  168. package/scripts/lib/project_config.rb +46 -0
  169. package/scripts/lib/project_validator.rb +3 -2
  170. package/scripts/lib/qmd_sync.rb +8 -7
  171. package/scripts/lib/ready_set.rb +462 -0
  172. package/scripts/lib/reference_archive.rb +45 -0
  173. package/scripts/lib/release_guard.rb +18 -0
  174. package/scripts/lib/report_screen.rb +1460 -74
  175. package/scripts/lib/rlm/corpus.rb +13 -0
  176. package/scripts/lib/rlm/probe.rb +29 -0
  177. package/scripts/lib/rlm/query.rb +22 -0
  178. package/scripts/lib/roadmap_graph.rb +210 -0
  179. package/scripts/lib/roadmap_migration.rb +95 -0
  180. package/scripts/lib/roadmap_queue.rb +158 -8
  181. package/scripts/lib/roadmap_render.rb +150 -0
  182. package/scripts/lib/roadmap_savepoint.rb +64 -14
  183. package/scripts/lib/runner_absorb.rb +703 -0
  184. package/scripts/lib/runner_answer.rb +206 -0
  185. package/scripts/lib/runner_core.rb +194 -0
  186. package/scripts/lib/runner_dispatch.rb +525 -0
  187. package/scripts/lib/runner_policy.rb +191 -0
  188. package/scripts/lib/runner_proposals.rb +275 -0
  189. package/scripts/lib/runner_rewind.rb +201 -0
  190. package/scripts/lib/runner_sweep.rb +231 -0
  191. package/scripts/lib/runner_until_empty.rb +252 -0
  192. package/scripts/lib/runner_watch.rb +389 -0
  193. package/scripts/lib/savepoint.rb +141 -19
  194. package/scripts/lib/scaffold_intent.rb +6 -3
  195. package/scripts/lib/screen_paint.rb +555 -0
  196. package/scripts/lib/screens/dashboard.rb +20 -0
  197. package/scripts/lib/screens/plan.rb +18 -0
  198. package/scripts/lib/screens/roadmap.rb +15 -0
  199. package/scripts/lib/search_index.rb +55 -0
  200. package/scripts/lib/session_close.rb +30 -28
  201. package/scripts/lib/session_git.rb +25 -18
  202. package/scripts/lib/session_ledger.rb +48 -4
  203. package/scripts/lib/session_usage.rb +190 -0
  204. package/scripts/lib/sqlite.rb +22 -0
  205. package/scripts/lib/stop_gate.rb +95 -0
  206. package/scripts/lib/store_discovery.rb +7 -6
  207. package/scripts/lib/store_layout.rb +54 -0
  208. package/scripts/lib/store_provisioning.rb +2 -1
  209. package/scripts/lib/store_sync.rb +85 -0
  210. package/scripts/lib/stores_move.rb +93 -0
  211. package/scripts/lib/verify_intent.rb +36 -8
  212. package/scripts/lib/version_number.rb +48 -0
  213. package/scripts/lib/work_graph.rb +59 -0
  214. package/scripts/lib/work_graph_validator.rb +201 -0
  215. package/scripts/lib/worktree.rb +27 -32
  216. package/scripts/lib/worktree_sweep.rb +6 -5
  217. package/scripts/link-suggest +2 -1
  218. package/scripts/meter-watch +57 -0
  219. package/scripts/migrate-to-global +1 -1
  220. package/scripts/new-intent +4 -13
  221. package/scripts/node-input +92 -0
  222. package/scripts/node-run +225 -0
  223. package/scripts/node-transition +291 -0
  224. package/scripts/outcome-report +74 -0
  225. package/scripts/plastic-lock +42 -33
  226. package/scripts/promote-session-item +3 -2
  227. package/scripts/read-config +53 -9
  228. package/scripts/ready-set +126 -0
  229. package/scripts/release-check +123 -0
  230. package/scripts/report-screen +192 -30
  231. package/scripts/roadmap-graph +119 -0
  232. package/scripts/roadmap-savepoint +7 -0
  233. package/scripts/runner +581 -0
  234. package/scripts/savepoint-note +11 -9
  235. package/scripts/session-commit +2 -1
  236. package/scripts/session-usage +56 -0
  237. package/scripts/skill-lint +115 -6
  238. package/scripts/spawn-preamble +2 -2
  239. package/scripts/update.rb +25 -4
  240. package/scripts/validate-work-graph +39 -0
  241. package/scripts/verify-intent +3 -2
  242. package/scripts/write-handoff +2 -1
  243. package/templates/agents.md +7 -7
  244. package/templates/config.yml +16 -9
  245. package/templates/dashboard-screen.md +22 -0
  246. package/templates/display-fixture.md +21 -0
  247. package/templates/graph.md +16 -0
  248. package/templates/index.md +1 -1
  249. package/templates/intent-screen.md +1 -1
  250. package/templates/node-decision.md +11 -0
  251. package/templates/node-research.md +13 -0
  252. package/templates/node-verify.md +13 -0
  253. package/templates/node-work.md +22 -0
  254. package/templates/outcome.md +15 -2
  255. package/templates/project.yml +1 -1
  256. package/templates/render.css +10 -0
  257. package/templates/report-plan.md +15 -0
  258. package/templates/report-roadmap-delivered.md +10 -0
  259. package/templates/report-roadmap-plan.md +9 -0
  260. package/templates/report-roadmap-state.md +9 -0
  261. package/templates/report-state.md +1 -1
  262. package/templates/roadmap.md +13 -0
  263. package/bin/plastic.js +0 -70
  264. package/scripts/lib/bridge.rb +0 -116
  265. package/skills/agent-advisor/SKILL.md +0 -92
  266. package/skills/auto/SKILL.md +0 -295
  267. package/skills/auto/evals/evals.json +0 -255
  268. package/skills/auto/references/end-tail.md +0 -66
  269. package/skills/auto/references/human-report-contract.md +0 -78
  270. package/skills/conventions/SKILL.md +0 -29
  271. package/skills/dashboard/SKILL.md +0 -169
  272. package/skills/dashboard/evals/evals.json +0 -38
  273. package/skills/dashboard/references/classification.md +0 -22
  274. package/skills/dashboard/templates/dashboard-global.md +0 -20
  275. package/skills/dashboard/templates/dashboard-project.md +0 -19
  276. package/skills/direct/SKILL.md +0 -66
  277. package/skills/direct/references/request-signals.md +0 -59
  278. package/skills/doctor/SKILL.md +0 -299
  279. package/skills/doctor/report.md +0 -102
  280. package/skills/feedback/SKILL.md +0 -98
  281. package/skills/feedback/references/transport-and-privacy.md +0 -65
  282. package/skills/feedback/report.md +0 -36
  283. package/skills/install/SKILL.md +0 -217
  284. package/skills/intent-continuing/SKILL.md +0 -154
  285. package/skills/intent-continuing/references/board-fill.md +0 -43
  286. package/skills/intent-continuing/references/boarding-matrix.md +0 -34
  287. package/skills/intent-continuing/references/context-management.md +0 -28
  288. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  289. package/skills/intent-creating/SKILL.md +0 -164
  290. package/skills/intent-creating/evals/evals.json +0 -72
  291. package/skills/intent-creating/references/lifecycle.md +0 -81
  292. package/skills/intent-creating/references/wikilinks.md +0 -8
  293. package/skills/intent-ending/SKILL.md +0 -173
  294. package/skills/intent-ending/evals/evals.json +0 -74
  295. package/skills/intent-executing/SKILL.md +0 -170
  296. package/skills/intent-executing/evals/evals.json +0 -66
  297. package/skills/intent-executing/implementer-prompt.md +0 -42
  298. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  299. package/skills/intent-speccing/SKILL.md +0 -130
  300. package/skills/intent-speccing/evals/evals.json +0 -126
  301. package/skills/intent-speccing/references/design-principles.md +0 -44
  302. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  303. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  304. package/skills/project-creating/SKILL.md +0 -162
  305. package/skills/project-creating/references/hubs-projects.md +0 -55
  306. package/skills/project-creating/references/project-scaffolding.md +0 -97
  307. package/skills/releasing/SKILL.md +0 -337
  308. package/skills/releasing/references/deprecations.md +0 -60
  309. package/skills/releasing/references/promotion-and-tagging.md +0 -66
  310. package/skills/releasing/references/release-lines.md +0 -105
  311. package/skills/roadmap/SKILL.md +0 -64
  312. package/skills/roadmap/references/file-format.md +0 -124
  313. package/skills/roadmap/references/operations.md +0 -112
  314. package/skills/rollback/SKILL.md +0 -91
  315. package/skills/tutorial/SKILL.md +0 -65
  316. package/skills/tutorial/evals/evals.json +0 -186
  317. package/skills/uninstall/SKILL.md +0 -75
  318. package/skills/update/SKILL.md +0 -126
  319. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  320. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  321. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  322. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -0,0 +1,839 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "yaml"
6
+ require "date"
7
+ require "time"
8
+ require "set"
9
+
10
+ # SkillCensus (intent 324): counts typed /plastic-* commands against
11
+ # agent-invoked Skill calls, so D19 (which Plastic verbs stay visible as
12
+ # slash commands) is ruled on measured use rather than taste.
13
+ #
14
+ # Two sources, never mixed. `HistoryScanner` reads ~/.claude/history.jsonl
15
+ # for typed commands: the transcript tree is pruned to about 45 days and
16
+ # would undercount typed use by two orders of magnitude. `TranscriptScanner`
17
+ # reads the ~/.claude/projects tree for agent invocations, loads,
18
+ # attribution, and mentions. `Tally` maps both sources through the name
19
+ # map, and `Report` renders the result as Markdown or JSON.
20
+ #
21
+ # Maintainer tool: `bin/plastic-skill-census` lives beside `bin/plastic-bench`
22
+ # and is never installed into ~/.plastic. Stdlib only, no network, no eval.
23
+ #
24
+ # This file and its CLI never resolve the running process's home directory
25
+ # or its home environment variable: `bin/` ships in the npm tarball, so a
26
+ # default rooted there would make a published executable read any
27
+ # installer's transcripts. `--history` and `--transcripts` are required,
28
+ # with no default.
29
+ module SkillCensus
30
+ DEFAULT_CUTOFF = "2026-09-02"
31
+
32
+ NAME_MAP = {}.freeze
33
+
34
+ # Retired without a successor (attested only).
35
+ RETIRED = [
36
+ "plastic-intent-discovering",
37
+ "plastic-intent-discovery",
38
+ "plastic-intent-curator",
39
+ "plastic-skill-creating",
40
+ "plastic-skill-evaluating",
41
+ "plastic-humanizer",
42
+ "plastic-intent-brainstorming",
43
+ "plastic-brainstorming",
44
+ "plastic-continuing",
45
+ "plastic-project-continuing",
46
+ "plastic-roadmap-continuing",
47
+ "plastic-intent-starting",
48
+ "plastic-intent-creating",
49
+ "plastic-intent-speccing",
50
+ "plastic-intent-planning",
51
+ "plastic-intent-executing",
52
+ "plastic-intent-ending",
53
+ "plastic-intent-continuing",
54
+ "plastic-install",
55
+ "plastic-uninstall",
56
+ "plastic-update",
57
+ "plastic-rollback",
58
+ "plastic-doctor",
59
+ "plastic-feedback",
60
+ "plastic-tutorial",
61
+ "plastic-conventions",
62
+ "plastic-project-creating",
63
+ "plastic-roadmap",
64
+ "plastic-dashboard",
65
+ "plastic-auto",
66
+ "plastic-direct",
67
+ "plastic-agent-advisor",
68
+ "plastic-releasing",
69
+ ].freeze
70
+
71
+ # ~/.plastic/scripts/* filenames, never skills (review 3). plastic-lock
72
+ # alone occurs 257 times in the live files; mapping it into plastic-doctor
73
+ # was the original map's worst counting bug.
74
+ SCRIPTS = [
75
+ "plastic-lock",
76
+ "plastic-savepoint",
77
+ "plastic-bench",
78
+ "plastic-statusline",
79
+ "plastic-session-start",
80
+ "plastic-code-gate",
81
+ ].freeze
82
+
83
+ # Agent names, never skills (review 3).
84
+ AGENTS = [
85
+ "plastic-advisor",
86
+ "plastic-faux-advisor",
87
+ "plastic-enforcer",
88
+ "plastic-executor",
89
+ "plastic-future-intent-researcher",
90
+ ].freeze
91
+
92
+ # Roster skills this instrument cannot observe through the Skill tool or a
93
+ # typed command, with the mechanism that reaches them instead (review 4).
94
+ # plastic-agent-advisor and plastic-direct were retired by intent 372 (family 5) into
95
+ # RETIRED above: their skill directories are gone, so Roster.load never yields a row for
96
+ # them any more, and a MECHANISMS entry that no row can reach is dead.
97
+ MECHANISMS = {
98
+ "plastic-conventions" => "read as references/*.md from inside other skills",
99
+ "plastic-feedback" => "routed by the SessionStart hook and PLASTIC.md",
100
+ }.freeze
101
+
102
+ PLASTIC_TYPED_RE = %r{\A\s*/(plastic[:-][a-z0-9][a-z0-9-]*)}.freeze
103
+ PLASTIC_TAGGED_RE = %r{<command-name>\s*/(plastic[:-][a-z0-9][a-z0-9-]*)</command-name>}.freeze
104
+ BUILTIN_TYPED_RE = %r{\A\s*/([a-zA-Z][a-zA-Z0-9_-]*)}.freeze
105
+ MENTION_SCAN_RE = /plastic[:-][a-z0-9][a-z0-9-]*/.freeze
106
+ LOAD_PREFIX = "Base directory for this skill:"
107
+
108
+ # The pre-npm plugin colon namespace maps onto the hyphen form before the
109
+ # name map is consulted (D10): plastic:update normalises to plastic-update.
110
+ def self.normalize(raw)
111
+ raw.sub(":", "-")
112
+ end
113
+
114
+ # One classified occurrence: a typed command, a call, a load, a mention.
115
+ # `project` is set only by HistoryScanner (history records carry it);
116
+ # `file` is set only by TranscriptScanner.
117
+ Event = Struct.new(:name, :date, :project, :file, :detail, keyword_init: true)
118
+
119
+ # -------------------------------------------------------------------
120
+ # Roster - the current skill directories, from SKILL.md frontmatter.
121
+ # -------------------------------------------------------------------
122
+ module Roster
123
+ Skill = Struct.new(:name, :user_invocable, keyword_init: true)
124
+
125
+ def self.load(skills_dir)
126
+ raise "skills dir not found: #{skills_dir}" unless File.directory?(skills_dir)
127
+
128
+ Dir.children(skills_dir).sort.filter_map do |entry|
129
+ dir = File.join(skills_dir, entry)
130
+ next unless File.directory?(dir)
131
+
132
+ skill_md = File.join(dir, "SKILL.md")
133
+ next unless File.file?(skill_md)
134
+
135
+ fm = frontmatter(File.read(skill_md))
136
+ Skill.new(name: "plastic-#{entry}", user_invocable: fm["user-invocable"] == true)
137
+ end
138
+ end
139
+
140
+ # skill_lint.rb's split: content.split("---", 3), parts[1] is the
141
+ # frontmatter block. A missing or unparsable block reads as no flags,
142
+ # which Roster.load turns into user_invocable: false, never a raise.
143
+ def self.frontmatter(content)
144
+ parts = content.split("---", 3)
145
+ return {} if parts.length < 3
146
+
147
+ YAML.safe_load(parts[1].to_s) || {}
148
+ rescue Psych::SyntaxError
149
+ {}
150
+ end
151
+ end
152
+
153
+ # -------------------------------------------------------------------
154
+ # HistoryScanner - typed commands from ~/.claude/history.jsonl.
155
+ # -------------------------------------------------------------------
156
+ class HistoryScanner
157
+ Result = Struct.new(:record_count, :first_seen, :last_seen, :typed, :mentions,
158
+ :builtins, :self_generated, :monthly, keyword_init: true)
159
+
160
+ def initialize(history_path, cutoff:)
161
+ @path = history_path
162
+ @cutoff = cutoff.is_a?(Date) ? cutoff : Date.parse(cutoff.to_s)
163
+ end
164
+
165
+ def scan
166
+ raise "history file not found: #{@path}" unless File.file?(@path)
167
+
168
+ record_count = 0
169
+ typed = []
170
+ mentions = []
171
+ builtins = Hash.new(0)
172
+ self_generated = []
173
+ first_seen = nil
174
+ last_seen = nil
175
+
176
+ File.foreach(@path) do |raw_line|
177
+ line = raw_line.strip
178
+ next if line.empty?
179
+
180
+ record = begin
181
+ JSON.parse(line)
182
+ rescue JSON::ParserError
183
+ next
184
+ end
185
+ record_count += 1
186
+
187
+ display = record["display"].to_s
188
+ date = ms_to_date(record["timestamp"])
189
+ project = record["project"]
190
+
191
+ if date
192
+ first_seen = date if first_seen.nil? || date < first_seen
193
+ last_seen = date if last_seen.nil? || date > last_seen
194
+ end
195
+ after_cutoff = date && date >= @cutoff
196
+
197
+ if (m = display.match(PLASTIC_TYPED_RE))
198
+ event = Event.new(name: SkillCensus.normalize(m[1]), date: date, project: project, detail: display)
199
+ after_cutoff ? self_generated << event : typed << event
200
+ elsif (m = display.match(BUILTIN_TYPED_RE))
201
+ builtins[m[1]] += 1 unless after_cutoff
202
+ elsif display.match(MENTION_SCAN_RE)
203
+ mentions << Event.new(name: nil, date: date, project: project, detail: display) unless after_cutoff
204
+ end
205
+ end
206
+
207
+ monthly = Hash.new(0)
208
+ typed.each { |e| monthly[e.date.strftime("%Y-%m")] += 1 if e.date }
209
+
210
+ Result.new(record_count: record_count, first_seen: first_seen, last_seen: last_seen,
211
+ typed: typed, mentions: mentions, builtins: builtins,
212
+ self_generated: self_generated, monthly: monthly)
213
+ end
214
+
215
+ private
216
+
217
+ def ms_to_date(ms)
218
+ return nil unless ms
219
+
220
+ Time.at(ms.to_f / 1000.0).utc.to_date
221
+ end
222
+ end
223
+
224
+ # -------------------------------------------------------------------
225
+ # TranscriptScanner - calls, loads, attribution, and mentions from the
226
+ # ~/.claude/projects tree.
227
+ # -------------------------------------------------------------------
228
+ class TranscriptScanner
229
+ Call = Struct.new(:name, :tool_use_id, :file, :date, keyword_init: true)
230
+ Corroboration = Struct.new(:matched, :disagreements, keyword_init: true)
231
+
232
+ Result = Struct.new(
233
+ :files_top, :files_subagent, :record_count, :unparsable_lines,
234
+ :first_seen, :last_seen,
235
+ :calls_main, :calls_agent, :other_skills,
236
+ :loads, :attributed, :mentions, :scripts, :agents,
237
+ :transcript_typed, :self_generated, :corroboration,
238
+ :self_generated_calls, :self_generated_loads,
239
+ :self_generated_attributed, :self_generated_mentions,
240
+ keyword_init: true
241
+ )
242
+
243
+ def initialize(transcripts_dir, cutoff:)
244
+ @dir = transcripts_dir
245
+ @cutoff = cutoff.is_a?(Date) ? cutoff : Date.parse(cutoff.to_s)
246
+ end
247
+
248
+ def scan
249
+ raise "transcripts dir not found: #{@dir}" unless File.directory?(@dir)
250
+
251
+ state = new_state
252
+ Dir.glob(File.join(@dir, "**", "*.jsonl")).sort.each { |path| scan_file(path, state) }
253
+ finish(state)
254
+ end
255
+
256
+ private
257
+
258
+ def new_state
259
+ {
260
+ files_top: 0, files_subagent: 0, record_count: 0, unparsable_lines: 0,
261
+ first_seen: nil, last_seen: nil,
262
+ calls_main: [], calls_agent: [],
263
+ other_skills: Hash.new { |h, k| h[k] = { calls: 0, loads: 0, attributed: 0 } },
264
+ loads: [], attributed: Hash.new(0), mentions: [], scripts: Hash.new(0), agents: Hash.new(0),
265
+ transcript_typed: [], self_generated: [],
266
+ self_generated_calls: 0, self_generated_loads: 0,
267
+ self_generated_attributed: 0, self_generated_mentions: 0,
268
+ seen_tool_use_ids: Set.new, seen_uuids: Set.new,
269
+ corroboration_by_id: Hash.new { |h, k| h[k] = {} },
270
+ }
271
+ end
272
+
273
+ def scan_file(path, state)
274
+ file_is_subagent = subagent_path?(path)
275
+ state[file_is_subagent ? :files_subagent : :files_top] += 1
276
+
277
+ File.foreach(path) do |raw_line|
278
+ line = raw_line.strip
279
+ next if line.empty?
280
+
281
+ record = begin
282
+ JSON.parse(line)
283
+ rescue JSON::ParserError
284
+ state[:unparsable_lines] += 1
285
+ next
286
+ end
287
+
288
+ uuid = record["uuid"]
289
+ next if uuid && !state[:seen_uuids].add?(uuid)
290
+
291
+ state[:record_count] += 1
292
+ classify_record(record, path, file_is_subagent, state)
293
+ end
294
+ end
295
+
296
+ def classify_record(record, path, file_is_subagent, state)
297
+ date = iso_to_date(record["timestamp"])
298
+ track_span(date, state)
299
+ after_cutoff = date && date >= @cutoff
300
+ record_is_subagent = file_is_subagent || record["isSidechain"] == true || record.key?("agentId")
301
+
302
+ if (attribution = record["attributionSkill"])
303
+ if after_cutoff
304
+ state[:self_generated_attributed] += 1
305
+ elsif plastic_name?(attribution)
306
+ state[:attributed][attribution] += 1
307
+ else
308
+ state[:other_skills][attribution][:attributed] += 1
309
+ end
310
+ end
311
+
312
+ classify_tool_use(record, path, date, record_is_subagent, after_cutoff, state) if record["type"] == "assistant"
313
+ classify_tool_result(record, after_cutoff, state)
314
+
315
+ return unless record["type"] == "user"
316
+
317
+ classify_user_text(record, path, date, after_cutoff, record_is_subagent, state)
318
+ end
319
+
320
+ def track_span(date, state)
321
+ return unless date
322
+
323
+ state[:first_seen] = date if state[:first_seen].nil? || date < state[:first_seen]
324
+ state[:last_seen] = date if state[:last_seen].nil? || date > state[:last_seen]
325
+ end
326
+
327
+ def classify_tool_use(record, path, date, record_is_subagent, after_cutoff, state)
328
+ each_content_block(record) do |block|
329
+ next unless block.is_a?(Hash) && block["type"] == "tool_use" && block["name"] == "Skill"
330
+
331
+ raw = block.dig("input", "skill").to_s
332
+ next if raw.empty?
333
+
334
+ # A post-cutoff block is counted only in the self-generated bucket and
335
+ # never touches seen_tool_use_ids, so it cannot consume an id a
336
+ # pre-cutoff block would later need (D16).
337
+ if after_cutoff
338
+ state[:self_generated_calls] += 1
339
+ next
340
+ end
341
+
342
+ id = block["id"]
343
+ next if id && !state[:seen_tool_use_ids].add?(id)
344
+
345
+ # Only the plastic: colon namespace maps onto the hyphen form (D10);
346
+ # a plugin-prefixed non-Plastic name like claudish-to-english:claudish
347
+ # must appear verbatim in other_skills, not split on its own colon.
348
+ name = raw.start_with?("plastic:") ? SkillCensus.normalize(raw) : raw
349
+ call = Call.new(name: name, tool_use_id: id, file: path, date: date)
350
+ if plastic_name?(name)
351
+ (record_is_subagent ? state[:calls_agent] : state[:calls_main]) << call
352
+ else
353
+ state[:other_skills][name][:calls] += 1
354
+ end
355
+ end
356
+ end
357
+
358
+ def classify_tool_result(record, after_cutoff, state)
359
+ return if after_cutoff
360
+
361
+ content = message_content(record)
362
+ return unless content.is_a?(Array)
363
+
364
+ block = content.find { |b| b.is_a?(Hash) && b["type"] == "tool_result" }
365
+ return unless block
366
+
367
+ tool_use_id = block["tool_use_id"]
368
+ return unless tool_use_id
369
+
370
+ if record["toolUseResult"].is_a?(Hash) && record["toolUseResult"]["commandName"]
371
+ state[:corroboration_by_id][tool_use_id][:command_name] = record["toolUseResult"]["commandName"]
372
+ end
373
+
374
+ text = block["content"]
375
+ text = text.filter_map { |c| c["text"] if c.is_a?(Hash) }.join if text.is_a?(Array)
376
+ if text.is_a?(String) && (m = text.match(/Launching skill:\s*(\S+)/))
377
+ state[:corroboration_by_id][tool_use_id][:launching_text] = m[1]
378
+ end
379
+ end
380
+
381
+ def classify_user_text(record, path, date, after_cutoff, record_is_subagent, state)
382
+ text = extract_text(record)
383
+ return if text.nil? || text.empty?
384
+
385
+ if (load_name = load_name_from(text))
386
+ if after_cutoff
387
+ state[:self_generated_loads] += 1
388
+ elsif plastic_name?(load_name)
389
+ state[:loads] << Event.new(name: load_name, date: date, file: path)
390
+ else
391
+ state[:other_skills][load_name][:loads] += 1
392
+ end
393
+ remainder = text.sub(/\A.*#{Regexp.escape(LOAD_PREFIX)}[^\n]*\n?/m, "")
394
+ scan_occurrences(remainder, path, date, after_cutoff, state)
395
+ return
396
+ end
397
+
398
+ typed_name = tagged_or_bare_typed_name(text)
399
+ if typed_name
400
+ event = Event.new(name: typed_name, date: date, file: path, detail: text[0, 200])
401
+ unless record_is_subagent
402
+ after_cutoff ? state[:self_generated] << event : state[:transcript_typed] << event
403
+ end
404
+ return
405
+ end
406
+
407
+ scan_occurrences(text, path, date, after_cutoff, state)
408
+ end
409
+
410
+ def scan_occurrences(text, path, date, after_cutoff, state)
411
+ matches = text.scan(MENTION_SCAN_RE)
412
+ if after_cutoff
413
+ state[:self_generated_mentions] += matches.length
414
+ return
415
+ end
416
+
417
+ matches.each do |raw|
418
+ name = SkillCensus.normalize(raw)
419
+ if SCRIPTS.include?(name)
420
+ state[:scripts][name] += 1
421
+ elsif AGENTS.include?(name)
422
+ state[:agents][name] += 1
423
+ else
424
+ state[:mentions] << Event.new(name: name, date: date, file: path)
425
+ end
426
+ end
427
+ end
428
+
429
+ def finish(state)
430
+ disagreements = state[:corroboration_by_id].filter_map do |tool_use_id, sides|
431
+ next unless sides[:command_name] && sides[:launching_text]
432
+ next if sides[:command_name] == sides[:launching_text]
433
+
434
+ { tool_use_id: tool_use_id, command_name: sides[:command_name], launching_text: sides[:launching_text] }
435
+ end
436
+ matched = state[:corroboration_by_id].count { |_, sides| sides[:command_name] }
437
+
438
+ Result.new(
439
+ files_top: state[:files_top], files_subagent: state[:files_subagent],
440
+ record_count: state[:record_count], unparsable_lines: state[:unparsable_lines],
441
+ first_seen: state[:first_seen], last_seen: state[:last_seen],
442
+ calls_main: state[:calls_main], calls_agent: state[:calls_agent], other_skills: state[:other_skills],
443
+ loads: state[:loads], attributed: state[:attributed], mentions: state[:mentions],
444
+ scripts: state[:scripts], agents: state[:agents],
445
+ transcript_typed: state[:transcript_typed], self_generated: state[:self_generated],
446
+ corroboration: Corroboration.new(matched: matched, disagreements: disagreements),
447
+ self_generated_calls: state[:self_generated_calls], self_generated_loads: state[:self_generated_loads],
448
+ self_generated_attributed: state[:self_generated_attributed],
449
+ self_generated_mentions: state[:self_generated_mentions]
450
+ )
451
+ end
452
+
453
+ def plastic_name?(name)
454
+ name.start_with?("plastic-")
455
+ end
456
+
457
+ def subagent_path?(path)
458
+ path.split(File::SEPARATOR).include?("subagents")
459
+ end
460
+
461
+ def iso_to_date(value)
462
+ return nil if value.nil?
463
+
464
+ Time.parse(value.to_s).to_date
465
+ rescue ArgumentError, TypeError
466
+ nil
467
+ end
468
+
469
+ def message_content(record)
470
+ record["message"].is_a?(Hash) ? record["message"]["content"] : record["content"]
471
+ end
472
+
473
+ def each_content_block(record)
474
+ content = message_content(record)
475
+ return unless content.is_a?(Array)
476
+
477
+ content.each { |block| yield block }
478
+ end
479
+
480
+ def extract_text(record)
481
+ content = message_content(record)
482
+ return content if content.is_a?(String)
483
+ return nil unless content.is_a?(Array)
484
+
485
+ content.filter_map { |b| b["text"] if b.is_a?(Hash) && b["type"] == "text" }.join("\n")
486
+ end
487
+
488
+ def load_name_from(text)
489
+ return nil unless text.lstrip.start_with?(LOAD_PREFIX)
490
+
491
+ path = text.lstrip.sub(LOAD_PREFIX, "").strip.lines.first.to_s.strip
492
+ File.basename(path)
493
+ end
494
+
495
+ def tagged_or_bare_typed_name(text)
496
+ if (m = text.match(PLASTIC_TAGGED_RE))
497
+ return SkillCensus.normalize(m[1])
498
+ end
499
+ if (m = text.match(PLASTIC_TYPED_RE))
500
+ return SkillCensus.normalize(m[1])
501
+ end
502
+
503
+ nil
504
+ end
505
+ end
506
+
507
+ # -------------------------------------------------------------------
508
+ # Tally - maps both sources' events onto the roster through NAME_MAP,
509
+ # RETIRED, and MECHANISMS.
510
+ # -------------------------------------------------------------------
511
+ class Tally
512
+ SkillRow = Struct.new(
513
+ :name, :user_invocable, :typed, :calls_main, :calls_agent, :loads,
514
+ :attributed, :mapped_from, :mechanism, :first_seen, :last_seen,
515
+ keyword_init: true
516
+ ) do
517
+ def evidence?
518
+ typed.to_i.positive? || calls_main.to_i.positive? || calls_agent.to_i.positive?
519
+ end
520
+ end
521
+
522
+ Built = Struct.new(:skills, :retired, :unmapped, :map_coverage, :history, :transcript, keyword_init: true)
523
+
524
+ def initialize(history_scan, transcript_scan, roster, name_map: NAME_MAP)
525
+ @name_map = name_map
526
+ @history = history_scan
527
+ @transcript = transcript_scan
528
+ @roster = roster
529
+ end
530
+
531
+ def build
532
+ rows = {}
533
+ @roster.each { |skill| rows[skill.name] = new_row(skill) }
534
+ retired_row = new_row(nil, name: "retired")
535
+ unmapped_row = new_row(nil, name: "unmapped")
536
+
537
+ apply = lambda do |raw_name, field, date = nil|
538
+ target = resolve(raw_name, rows)
539
+ row = case target
540
+ when :retired then retired_row
541
+ when :unmapped then unmapped_row
542
+ else rows[target]
543
+ end
544
+ row[field] += 1
545
+ row.mapped_from[raw_name] += 1 if target != raw_name
546
+ track_row_span(row, date)
547
+ end
548
+
549
+ @history.typed.each { |event| apply.call(event.name, :typed, event.date) }
550
+ @transcript.calls_main.each { |call| apply.call(call.name, :calls_main, call.date) }
551
+ @transcript.calls_agent.each { |call| apply.call(call.name, :calls_agent, call.date) }
552
+ @transcript.loads.each { |event| apply.call(event.name, :loads, event.date) }
553
+ @transcript.attributed.each { |name, count| count.times { apply.call(name, :attributed) } }
554
+
555
+ map_coverage = @name_map.map do |raw, target|
556
+ { raw: raw, target: target, observed: rows[target]&.mapped_from&.fetch(raw, 0) || 0 }
557
+ end
558
+
559
+ Built.new(skills: @roster.map { |s| rows[s.name] }, retired: retired_row, unmapped: unmapped_row,
560
+ map_coverage: map_coverage, history: @history, transcript: @transcript)
561
+ end
562
+
563
+ private
564
+
565
+ def new_row(skill, name: nil)
566
+ SkillRow.new(
567
+ name: skill&.name || name, user_invocable: skill&.user_invocable,
568
+ typed: 0, calls_main: 0, calls_agent: 0, loads: 0, attributed: 0,
569
+ mapped_from: Hash.new(0), mechanism: skill && MECHANISMS[skill.name]
570
+ )
571
+ end
572
+
573
+ def resolve(raw_name, rows)
574
+ return raw_name if rows.key?(raw_name)
575
+
576
+ target = @name_map[raw_name]
577
+ return target if target && rows.key?(target)
578
+ return :retired if RETIRED.include?(raw_name)
579
+
580
+ :unmapped
581
+ end
582
+
583
+ def track_row_span(row, date)
584
+ return unless date
585
+
586
+ row.first_seen = date if row.first_seen.nil? || date < row.first_seen
587
+ row.last_seen = date if row.last_seen.nil? || date > row.last_seen
588
+ end
589
+ end
590
+
591
+ # -------------------------------------------------------------------
592
+ # Report - Markdown and JSON rendering.
593
+ # -------------------------------------------------------------------
594
+ module Report
595
+ module_function
596
+
597
+ def markdown(built, history_path: nil, transcripts_path: nil, cutoff: nil)
598
+ lines = []
599
+ lines.concat(header(history_path, transcripts_path, cutoff))
600
+ lines.concat(method_section(cutoff))
601
+ lines.concat(summary_table(built))
602
+ lines.concat(per_skill_sections(built))
603
+ lines.concat(context_tables(built))
604
+ lines.concat(cross_check_section(built))
605
+ lines.concat(self_generated_section(built))
606
+ lines.concat(typed_events_section(built))
607
+ lines.join("\n") + "\n"
608
+ end
609
+
610
+ def header(history_path, transcripts_path, cutoff)
611
+ lines = ["# Plastic skill usage census", ""]
612
+ lines << "- history: #{history_path}" if history_path
613
+ lines << "- transcripts: #{transcripts_path}" if transcripts_path
614
+ lines << "- cutoff: #{cutoff}" if cutoff
615
+ lines << ""
616
+ lines
617
+ end
618
+
619
+ def method_section(cutoff)
620
+ [
621
+ "## Method",
622
+ "",
623
+ "- Typed command: a history.jsonl record whose display matches a leading /plastic- or /plastic: form; " \
624
+ "a mid-sentence or backticked mention is excluded and tallied separately.",
625
+ "- Calls: a Skill tool_use block, split into calls_main (top-level transcript file) and calls_agent " \
626
+ "(subagent transcript file), because the auto pipeline dispatches its own skills.",
627
+ "- Loads: a user record whose text begins \"Base directory for this skill:\".",
628
+ "- Attribution: attributionSkill on a record measures time spent under a skill, not invocations, and " \
629
+ "never feeds the call count.",
630
+ "- Cutoff: records dated on or after #{cutoff} are excluded from every count and reported on the " \
631
+ "Self-generated line.",
632
+ "",
633
+ ]
634
+ end
635
+
636
+ def summary_table(built)
637
+ lines = [
638
+ "## Summary",
639
+ "",
640
+ "| skill | user_invocable | typed | calls_main | calls_agent | loads | attributed | evidence |",
641
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
642
+ ]
643
+ (built.skills + [built.retired, built.unmapped]).each { |row| lines << summary_row(row) }
644
+ lines << ""
645
+ lines
646
+ end
647
+
648
+ def summary_row(row)
649
+ if row.mechanism && !row.evidence?
650
+ "| #{row.name} | #{fmt(row.user_invocable)} | n/a | n/a | n/a | #{row.loads} | #{row.attributed} | n/a - #{row.mechanism} |"
651
+ else
652
+ "| #{row.name} | #{fmt(row.user_invocable)} | #{row.typed} | #{row.calls_main} | #{row.calls_agent} | " \
653
+ "#{row.loads} | #{row.attributed} | #{row.evidence? ? "measured" : "zero"} |"
654
+ end
655
+ end
656
+
657
+ def fmt(value)
658
+ value.nil? ? "-" : value.to_s
659
+ end
660
+
661
+ def per_skill_sections(built)
662
+ lines = ["## Per-skill", ""]
663
+ built.skills.each do |row|
664
+ lines << "## #{row.name}"
665
+ lines << ""
666
+ lines << "| field | value |"
667
+ lines << "| --- | --- |"
668
+ if row.mechanism && !row.evidence?
669
+ lines << "| evidence | n/a |"
670
+ lines << "| mechanism | #{row.mechanism} |"
671
+ else
672
+ lines << "| typed | #{row.typed} |"
673
+ lines << "| calls_main | #{row.calls_main} |"
674
+ lines << "| calls_agent | #{row.calls_agent} |"
675
+ end
676
+ lines << "| loads | #{row.loads} |"
677
+ lines << "| attributed | #{row.attributed} (time spent under this skill, not invocations) |"
678
+ unless row.mapped_from.empty?
679
+ mapped = row.mapped_from.map { |name, count| "#{name} (#{count})" }.join(", ")
680
+ lines << "| mapped from | #{mapped} |"
681
+ end
682
+ lines << "| first seen | #{row.first_seen} |" if row.first_seen
683
+ lines << "| last seen | #{row.last_seen} |" if row.last_seen
684
+ lines << ""
685
+ end
686
+ lines
687
+ end
688
+
689
+ def context_tables(built)
690
+ transcript = built.transcript
691
+ history = built.history
692
+ lines = ["## Built-in commands", "", "| command | count |", "| --- | --- |"]
693
+ history.builtins.sort.each { |name, count| lines << "| /#{name} | #{count} |" }
694
+ lines << ""
695
+
696
+ lines << "## Other skills"
697
+ lines << ""
698
+ lines << "| skill | calls | loads | attributed |"
699
+ lines << "| --- | --- | --- | --- |"
700
+ transcript.other_skills.sort.each do |name, counts|
701
+ lines << "| #{name} | #{counts[:calls]} | #{counts[:loads]} | #{counts[:attributed]} |"
702
+ end
703
+ lines << ""
704
+
705
+ lines << "## Scripts"
706
+ lines << ""
707
+ lines << "| script | occurrences |"
708
+ lines << "| --- | --- |"
709
+ SCRIPTS.each { |name| lines << "| #{name} | #{transcript.scripts.fetch(name, 0)} |" }
710
+ lines << ""
711
+
712
+ lines << "## Agents"
713
+ lines << ""
714
+ lines << "| agent | occurrences |"
715
+ lines << "| --- | --- |"
716
+ AGENTS.each { |name| lines << "| #{name} | #{transcript.agents.fetch(name, 0)} |" }
717
+ lines << ""
718
+
719
+ lines << "## Map coverage"
720
+ lines << ""
721
+ lines << "| raw name | maps to | observed |"
722
+ lines << "| --- | --- | --- |"
723
+ built.map_coverage.each { |entry| lines << "| #{entry[:raw]} | #{entry[:target]} | #{entry[:observed]} |" }
724
+ lines << ""
725
+ lines << "Observed sums typed, calls (main plus agent), loads, and attributed occurrences for " \
726
+ "that raw name; it is not a single dimension."
727
+ lines << ""
728
+ lines
729
+ end
730
+
731
+ def cross_check_section(built)
732
+ history = built.history
733
+ transcript = built.transcript
734
+ window_start = transcript.first_seen
735
+ history_in_window = if window_start
736
+ history.typed.count { |e| e.date && e.date >= window_start }
737
+ else
738
+ 0
739
+ end
740
+ transcript_typed_total = transcript.transcript_typed.length
741
+ gap = history_in_window.zero? ? 0 : ((history_in_window - transcript_typed_total).abs.to_f / history_in_window * 100).round(1)
742
+
743
+ [
744
+ "## Cross-check",
745
+ "",
746
+ "History typed commands inside the transcript window (from #{window_start}): #{history_in_window}.",
747
+ "Transcript-typed events over the same window: #{transcript_typed_total}.",
748
+ "Gap: #{gap} percent.",
749
+ "",
750
+ ]
751
+ end
752
+
753
+ def self_generated_section(built)
754
+ history_self = built.history.self_generated
755
+ transcript = built.transcript
756
+ [
757
+ "## Self-generated",
758
+ "",
759
+ "History records excluded by the cutoff: #{history_self.length}.",
760
+ "",
761
+ "Transcript records excluded by the cutoff, per dimension (D16: the cutoff gates " \
762
+ "every count, not only typed commands):",
763
+ "",
764
+ "| dimension | excluded |",
765
+ "| --- | --- |",
766
+ "| typed | #{transcript.self_generated.length} |",
767
+ "| mentions | #{transcript.self_generated_mentions.to_i} |",
768
+ "| calls | #{transcript.self_generated_calls.to_i} |",
769
+ "| loads | #{transcript.self_generated_loads.to_i} |",
770
+ "| attributed | #{transcript.self_generated_attributed.to_i} |",
771
+ "",
772
+ ]
773
+ end
774
+
775
+ def typed_events_section(built)
776
+ lines = ["## Typed events", "", "| name | date | project |", "| --- | --- | --- |"]
777
+ built.history.typed.each { |event| lines << "| #{event.name} | #{event.date} | #{event.project} |" }
778
+ lines << ""
779
+ lines
780
+ end
781
+
782
+ def json(built, history_path: nil, transcripts_path: nil, cutoff: nil)
783
+ JSON.pretty_generate(build_data(built, history_path: history_path, transcripts_path: transcripts_path, cutoff: cutoff))
784
+ end
785
+
786
+ def build_data(built, history_path: nil, transcripts_path: nil, cutoff: nil)
787
+ {
788
+ "history_path" => history_path,
789
+ "transcripts_path" => transcripts_path,
790
+ "cutoff" => cutoff,
791
+ "skills" => (built.skills + [built.retired, built.unmapped]).map { |row| row_data(row) },
792
+ "map_coverage" => built.map_coverage,
793
+ "history" => {
794
+ "record_count" => built.history.record_count,
795
+ "first_seen" => built.history.first_seen&.to_s,
796
+ "last_seen" => built.history.last_seen&.to_s,
797
+ "typed_total" => built.history.typed.length,
798
+ "monthly" => built.history.monthly,
799
+ "builtins" => built.history.builtins,
800
+ "self_generated" => built.history.self_generated.length,
801
+ },
802
+ "transcript" => {
803
+ "files_top" => built.transcript.files_top,
804
+ "files_subagent" => built.transcript.files_subagent,
805
+ "record_count" => built.transcript.record_count,
806
+ "unparsable_lines" => built.transcript.unparsable_lines,
807
+ "first_seen" => built.transcript.first_seen&.to_s,
808
+ "last_seen" => built.transcript.last_seen&.to_s,
809
+ "calls_main" => built.transcript.calls_main.length,
810
+ "calls_agent" => built.transcript.calls_agent.length,
811
+ "other_skills" => built.transcript.other_skills,
812
+ "loads" => built.transcript.loads.length,
813
+ "attributed" => built.transcript.attributed,
814
+ "mentions" => built.transcript.mentions.length,
815
+ "scripts" => built.transcript.scripts,
816
+ "agents" => built.transcript.agents,
817
+ "transcript_typed" => built.transcript.transcript_typed.length,
818
+ "self_generated" => built.transcript.self_generated.length,
819
+ "self_generated_mentions" => built.transcript.self_generated_mentions.to_i,
820
+ "self_generated_calls" => built.transcript.self_generated_calls.to_i,
821
+ "self_generated_loads" => built.transcript.self_generated_loads.to_i,
822
+ "self_generated_attributed" => built.transcript.self_generated_attributed.to_i,
823
+ "corroboration_matched" => built.transcript.corroboration.matched,
824
+ "corroboration_disagreements" => built.transcript.corroboration.disagreements,
825
+ },
826
+ }
827
+ end
828
+
829
+ def row_data(row)
830
+ {
831
+ "name" => row.name, "user_invocable" => row.user_invocable,
832
+ "typed" => row.typed, "calls_main" => row.calls_main, "calls_agent" => row.calls_agent,
833
+ "loads" => row.loads, "attributed" => row.attributed,
834
+ "mapped_from" => row.mapped_from, "mechanism" => row.mechanism,
835
+ "first_seen" => row.first_seen&.to_s, "last_seen" => row.last_seen&.to_s,
836
+ }
837
+ end
838
+ end
839
+ end