@zalom/plastic 2.0.0-alpha.9 → 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 +11 -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 +55 -2
  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 +153 -30
  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 +7 -0
  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 +23 -14
  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 +531 -71
  152. package/scripts/lib/intent_screen.rb +4 -4
  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 +350 -54
  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 +1371 -47
  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 +365 -28
  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 +177 -16
  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 +8 -3
  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 -296
  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 -176
  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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "tempfile"
5
+ require_relative "search_index"
6
+ require_relative "reference_archive"
7
+ require_relative "work_graph"
8
+
9
+ module Plastic
10
+ module StoreSync
11
+ INTENT_PATH = %r{\A(?:(?:projects|stores)/([^/]+)/)?store/([^/]+?)--}
12
+ ROW_HASH = "lower(hex(sha3(sqlar_uncompress(data, sz), 256)))"
13
+
14
+ def self.action(state)
15
+ file, row, recorded = state.values_at("file", "row", "hash")
16
+ return "write" if file.nil?
17
+ return if file == row
18
+ return "read" if row == recorded
19
+ return "write" if file == recorded
20
+
21
+ "conflict"
22
+ end
23
+
24
+ def self.plan(home)
25
+ known = states(home)
26
+ fresh = SearchIndex.files(home) - known.map { |state| state["path"] }
27
+ plan = known.map { |state| [action(state), state["path"]] } + fresh.map { |file| ["read", file] }
28
+ plan.select(&:first).sort_by(&:last)
29
+ end
30
+
31
+ def self.states(home)
32
+ Sqlite.call(SearchIndex.path(home), <<~SQL, readonly: true)
33
+ SELECT path, lower(hex(sha3(readfile(#{Sqlite.quote(home)} || '/' || path), 256))) AS file, #{ROW_HASH} AS row, hash
34
+ FROM doc;
35
+ SQL
36
+ end
37
+
38
+ def self.apply(home, plan)
39
+ statements = plan.map { |action, path| send(action, home, path) }
40
+ statements << "INSERT INTO ft(ft) VALUES('rebuild');" if plan.any? { |action, _path| action == "read" }
41
+ Sqlite.call(SearchIndex.path(home), ["BEGIN;", *statements, "COMMIT;"].join("\n"))
42
+ end
43
+
44
+ def self.read(home, path)
45
+ "DELETE FROM doc WHERE path = #{Sqlite.quote(path)};\n#{SearchIndex.insert(home, path)}"
46
+ end
47
+
48
+ def self.write(home, path)
49
+ where = "WHERE path = #{Sqlite.quote(path)}"
50
+ "#{restore(home, "doc", "path")} #{where};\nUPDATE doc SET hash = #{ROW_HASH} #{where};"
51
+ end
52
+
53
+ def self.restore(home, table, column)
54
+ "SELECT writefile(#{Sqlite.quote(home)} || '/' || #{column}, sqlar_uncompress(data, sz)) FROM #{table}"
55
+ end
56
+
57
+ def self.diff(home, path)
58
+ body = Sqlite.call(SearchIndex.path(home), <<~SQL, readonly: true).first["body"]
59
+ SELECT CAST(sqlar_uncompress(data, sz) AS TEXT) AS body FROM doc WHERE path = #{Sqlite.quote(path)};
60
+ SQL
61
+ Tempfile.create("plastic-row") do |row|
62
+ row.write(body)
63
+ row.flush
64
+ Open3.capture2("diff", "-u", "-L", "row", "-L", "file", row.path, File.join(home, path)).first
65
+ end
66
+ end
67
+
68
+ def self.orphans(home)
69
+ paths = Sqlite.call(SearchIndex.path(home), "SELECT path FROM doc;", readonly: true) +
70
+ Sqlite.call(ReferenceArchive.path(home), "SELECT name AS path FROM sqlar;", readonly: true)
71
+ keys = paths.filter_map { |row| row["path"].match(INTENT_PATH) }.map { |found| "#{found[1] || "global"}:#{found[2]}" }
72
+ (keys.uniq - WorkGraph.known(home)).sort
73
+ end
74
+
75
+ def self.checkout(home)
76
+ {SearchIndex.path(home) => %w[doc path], ReferenceArchive.path(home) => %w[sqlar name]}.flat_map do |database, (table, column)|
77
+ missing = Sqlite.call(database, "SELECT #{column} AS path FROM #{table};", readonly: true)
78
+ .map { |row| row["path"] }.reject { |path| File.exist?(File.join(home, path)) }
79
+ names = missing.map { |path| Sqlite.quote(path) }.join(", ")
80
+ Sqlite.call(database, "#{restore(home, table, column)} WHERE #{column} IN (#{names});", readonly: true)
81
+ missing
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "store_layout"
5
+ require_relative "sqlite"
6
+ require_relative "lock"
7
+
8
+ module Plastic
9
+ class StoresMove
10
+ Refused = Class.new(StandardError)
11
+
12
+ GLOBAL_ENTRIES = %w[store INDEX.md roadmaps].freeze
13
+ PATH_COLUMNS = {"knowledge_graph.db" => %w[doc path], "references.db" => %w[sqlar name]}.freeze
14
+ LOCKS = "{store,projects/*/store}/*/delivery.lock"
15
+
16
+ def initialize(home, qmd_index:, dry_run: false)
17
+ @home = home
18
+ @qmd_index = qmd_index
19
+ @dry_run = dry_run
20
+ end
21
+
22
+ def call
23
+ refuse
24
+ plan = [["copy", copy]] + moves.map { |from, to| ["move", "#{from} -> #{to}"] } + rewrites.map { |file| ["rewrite", file] }
25
+ run unless @dry_run
26
+ plan
27
+ end
28
+
29
+ def copy
30
+ "#{@home}-before-stores-move"
31
+ end
32
+
33
+ private
34
+
35
+ def refuse
36
+ raise Refused, "#{File.join(@home, "stores")}/ exists, so this home has moved already" if StoreLayout.moved?(@home)
37
+ raise Refused, "a delivery lock is held: #{held.join(", ")}" unless held.empty?
38
+ raise Refused, "#{copy} exists; remove it or move it away first" if File.exist?(copy)
39
+ end
40
+
41
+ def held
42
+ Dir.glob(LOCKS, base: @home).select { |lock| Time.now - File.mtime(File.join(@home, lock)) < Lock::TTL_SECONDS }
43
+ end
44
+
45
+ def moves
46
+ @moves ||= GLOBAL_ENTRIES.select { |name| File.exist?(File.join(@home, name)) }.map { |name| [name, "stores/global/#{name}"] } +
47
+ project_entries.map { |entry| ["projects/#{entry}", "stores/#{entry}"] }
48
+ end
49
+
50
+ def project_entries
51
+ Dir.exist?(projects) ? Dir.children(projects).sort : []
52
+ end
53
+
54
+ def projects
55
+ File.join(@home, "projects")
56
+ end
57
+
58
+ def rewrites
59
+ [File.join(@home, "config.yml"), @qmd_index, *PATH_COLUMNS.keys.map { |name| File.join(@home, name) }].select { |file| File.file?(file) }
60
+ end
61
+
62
+ def run
63
+ files = rewrites
64
+ FileUtils.cp_r(@home, copy, preserve: true)
65
+ moves.each { |from, to| move(from, to) }
66
+ FileUtils.rmdir(projects) if Dir.exist?(projects)
67
+ files.each { |file| PATH_COLUMNS.key?(File.basename(file)) ? rewrite_rows(file) : rewrite_text(file) }
68
+ end
69
+
70
+ def move(from, to)
71
+ FileUtils.mkdir_p(File.dirname(File.join(@home, to)))
72
+ FileUtils.mv(File.join(@home, from), File.join(@home, to))
73
+ end
74
+
75
+ def rewrite_text(file)
76
+ FileUtils.cp(file, "#{file}.before-stores-move", preserve: true)
77
+ File.write(file, [@home, "~/.plastic"].inject(File.read(file)) { |text, base| relocate(text, base) })
78
+ end
79
+
80
+ def relocate(text, base)
81
+ text.gsub(%r{#{Regexp.escape(base)}/(store|roadmaps|INDEX\.md)\b}) { "#{base}/stores/global/#{Regexp.last_match(1)}" }
82
+ .gsub(%r{#{Regexp.escape(base)}/projects\b}) { "#{base}/stores" }
83
+ end
84
+
85
+ def rewrite_rows(file)
86
+ table, column = PATH_COLUMNS.fetch(File.basename(file))
87
+ Sqlite.call(file, <<~SQL)
88
+ UPDATE #{table} SET #{column} = 'stores/' || substr(#{column}, 10) WHERE #{column} LIKE 'projects/%';
89
+ UPDATE #{table} SET #{column} = 'stores/global/' || #{column} WHERE #{column} LIKE 'store/%';
90
+ SQL
91
+ end
92
+ end
93
+ end
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require_relative "store_layout"
4
5
  require_relative "worktree"
5
6
  require_relative "scaffold_intent"
6
7
 
@@ -9,7 +10,7 @@ require_relative "scaffold_intent"
9
10
  # (`Doctor#run_intent_check`), a net-new added-line em-dash diff guard (the first standing
10
11
  # implementation of this check; the only prior automated em-dash check,
11
12
  # `test/skill_command_lint_test.rb`, asserts against two FIXED file sets and cannot scan a
12
- # diff), a diffstat, and an optional caller-supplied `--suite` command folded into the same
13
+ # diff), a diffstat, and an optional caller-supplied `--suite` command merged into the same
13
14
  # verdict.
14
15
  #
15
16
  # Repo resolution and base-branch detection are NOT re-implemented here: `resolve_repo_dir`,
@@ -70,6 +71,10 @@ module VerifyIntent
70
71
  lines.concat(diffstat_lines)
71
72
  checks[:diffstat] = diffstat_check
72
73
 
74
+ report_lines_out, report_check = run_report_lines_check(intent_dir: intent_dir)
75
+ lines.concat(report_lines_out)
76
+ checks[:report] = report_check
77
+
73
78
  if Worktree.blank?(suite)
74
79
  checks[:suite] = { status: "skipped" }
75
80
  else
@@ -92,13 +97,7 @@ module VerifyIntent
92
97
  # shapes are told apart by whether store's grandparent is literally named "projects" (the
93
98
  # StoreDiscovery convention), never by hardcoding ".plastic" as a name.
94
99
  def resolve_plastic_home_and_scope(store)
95
- parent = File.dirname(store)
96
- grandparent = File.dirname(parent)
97
- if File.basename(grandparent) == "projects"
98
- [File.dirname(grandparent), "project:#{File.basename(parent)}"]
99
- else
100
- [parent, "global"]
101
- end
100
+ Plastic::StoreLayout.home_and_scope(store)
102
101
  end
103
102
 
104
103
  # --- check 1: doctor, scoped to the intent --------------------------------------
@@ -235,6 +234,35 @@ module VerifyIntent
235
234
  end
236
235
  end
237
236
 
237
+ # --- check: the Report savepoint lines (intent 331f, F17) ------------------------
238
+
239
+ REPORT_LINE_RE = /\A(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ)\s{2,}(\S+)\s{2,}(.+?)\s*\z/.freeze
240
+
241
+ # Every `Report`-kind savepoint line for this intent, oldest first: [timestamp, text].
242
+ # Never fails or gates anything (D3: the diffstat check already prints a summary block,
243
+ # this merges into the same verdict so verify-intent surfaces them too) - a delivery with
244
+ # no Report line is visible to `doctor`'s intent_reports_printed_check instead.
245
+ def report_lines(intent_dir)
246
+ path = File.join(intent_dir, "savepoint.md")
247
+ return [] unless File.exist?(path)
248
+
249
+ File.readlines(path).filter_map do |line|
250
+ m = line.strip.match(REPORT_LINE_RE)
251
+ next nil unless m && m[2] == "Report"
252
+ [m[1], m[3]]
253
+ end
254
+ end
255
+
256
+ def run_report_lines_check(intent_dir:)
257
+ entries = report_lines(intent_dir)
258
+ if entries.empty?
259
+ [["report lines: none recorded"], { status: "pass", lines: [] }]
260
+ else
261
+ lines = ["report lines:"] + entries.map { |ts, text| "#{ts} Report #{text}" }
262
+ [lines, { status: "pass", lines: entries }]
263
+ end
264
+ end
265
+
238
266
  # --- check 4: the optional suite --------------------------------------------------
239
267
 
240
268
  # The supplied command is very likely a ruby command, so it is spawned with RUBYOPT
@@ -0,0 +1,48 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # VersionNumber - dotted version comparison without RubyGems (intent 363).
5
+ #
6
+ # The launcher starts as `ruby --disable-gems`, which saves about 26 of the 40
7
+ # milliseconds a plain Ruby process costs, so no file a command loads may reach
8
+ # for Gem::Version. Parses the leading run of dotted integers, ignoring a
9
+ # leading "v" and anything after the digits, and compares two of them segment by
10
+ # segment with the shorter one zero-padded. A string with no leading integer
11
+ # parses to nil, so a caller answers "not found" instead of raising.
12
+ #
13
+ # It deliberately does not order pre-release suffixes: 2.0.0-alpha.27 and 2.0.0
14
+ # compare equal here. A caller that needs RubyGems' pre-release order, such as
15
+ # the release-tag reader in scripts/report-screen, keeps Gem::Version.
16
+ class VersionNumber
17
+ include Comparable
18
+
19
+ LEADING_DIGITS = /\A(\d+(?:\.\d+)*)/
20
+
21
+ attr_reader :segments
22
+
23
+ def self.parse(value)
24
+ match = LEADING_DIGITS.match(value.to_s.strip.sub(/\Av/, ""))
25
+ match && new(match[1].split(".").map(&:to_i))
26
+ end
27
+
28
+ def initialize(segments)
29
+ @segments = segments
30
+ end
31
+
32
+ def <=>(other)
33
+ return nil unless other.is_a?(VersionNumber)
34
+
35
+ width = [segments.length, other.segments.length].max
36
+ padded(width) <=> other.padded(width)
37
+ end
38
+
39
+ def to_s
40
+ segments.join(".")
41
+ end
42
+
43
+ protected
44
+
45
+ def padded(width)
46
+ segments + Array.new(width - segments.length, 0)
47
+ end
48
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "sqlite"
5
+ require_relative "index_entry"
6
+ require_relative "store_discovery"
7
+
8
+ module Plastic
9
+ module WorkGraph
10
+ NAME = "work_graph.db"
11
+ SCHEMA = <<~SQL
12
+ CREATE TABLE intent(id TEXT, store TEXT, slug TEXT, status TEXT, title TEXT);
13
+ CREATE TABLE ledger(intent_id TEXT, store TEXT, at TEXT, stage TEXT, text TEXT);
14
+ SQL
15
+ STATUSES = %w[active future completed abandoned].freeze
16
+ LEDGER_LINE = /\A(\S+)\s{2,}(\S+)\s{2,}(.+)\z/
17
+
18
+ def self.path(home)
19
+ File.join(home, NAME)
20
+ end
21
+
22
+ def self.build(home)
23
+ draft = "#{path(home)}.tmp"
24
+ FileUtils.rm_f(draft)
25
+ rows = StoreDiscovery.discover(home)[:stores].flat_map { |store| intents(store) + ledger(store) }
26
+ Sqlite.call(draft, [SCHEMA, "BEGIN;", *rows, "COMMIT;"].join("\n"))
27
+ File.rename(draft, path(home))
28
+ end
29
+
30
+ def self.intents(store)
31
+ return [] unless File.exist?(store[:index])
32
+
33
+ status = nil
34
+ File.readlines(store[:index], chomp: true, encoding: "UTF-8").filter_map do |line|
35
+ status = line.delete_prefix("## ").downcase if line.start_with?("## ")
36
+ entry = STATUSES.include?(status) && IndexEntry.match(line)
37
+ entry && insert("intent", entry[1], store[:slug], File.basename(File.dirname(entry[3])).split("--", 2).last, status, entry[2])
38
+ end
39
+ end
40
+
41
+ def self.ledger(store)
42
+ Dir.glob("*/savepoint.md", base: store[:store]).sort.flat_map do |file|
43
+ id = file.split("--").first
44
+ File.readlines(File.join(store[:store], file), chomp: true, encoding: "UTF-8").filter_map do |line|
45
+ entry = line.match(LEDGER_LINE)
46
+ entry && insert("ledger", id, store[:slug], entry[1], entry[2], entry[3])
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.insert(table, *values)
52
+ "INSERT INTO #{table} VALUES(#{values.map { |value| Sqlite.quote(value.to_s) }.join(", ")});"
53
+ end
54
+
55
+ def self.known(home)
56
+ Sqlite.call(path(home), "SELECT store, id FROM intent;", readonly: true).map { |row| "#{row["store"]}:#{row["id"]}" }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,201 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "graph_file"
5
+ require_relative "graph_edges"
6
+ require_relative "node_file"
7
+ require_relative "action_graph_shim"
8
+
9
+ # WorkGraphValidator (intent 334, n4): the in-batch reader over one intent's
10
+ # graph.md and nodes/ - 327's rule for budget, files, and the decision and
11
+ # research kinds (review A17). {ok:, missing:, errors:}, the same Result shape
12
+ # ProjectValidator returns (review B7). Named apart from
13
+ # IntentValidator#validate_graph, which already exists for the KNOWLEDGE
14
+ # graph and stays unrelated to this one under D40 (review B9).
15
+ #
16
+ # Built over GraphFile, GraphEdges, and NodeFile only - never IntentValidator,
17
+ # never ReportScreen. Every check accumulates into `errors` rather than
18
+ # short-circuiting on the first one (see: "the owner fixes one thing per
19
+ # run").
20
+ module WorkGraphValidator
21
+ module_function
22
+
23
+ def validate(intent_dir)
24
+ missing = []
25
+ errors = []
26
+
27
+ graph_path = File.join(intent_dir, "graph.md")
28
+ parsed = GraphFile.parse(graph_path)
29
+ errors.concat(parsed[:errors])
30
+
31
+ graph = parsed[:graph]
32
+ if graph.nil?
33
+ # G9 (intent 342, D6/D7/D12): a legacy intent that never got a
34
+ # graph.md still reads as a valid work graph, structurally, through
35
+ # the backward shim - but only when graph.md is genuinely absent.
36
+ # A graph.md that exists but is malformed (no "## Graph" section)
37
+ # must never fall through to the synthetic chain and hide its own
38
+ # error (D12's whole point). The fresh error list here is
39
+ # deliberate: `errors` above already holds the not-found error from
40
+ # `parsed[:errors]`, and reusing it would return ok: false for every
41
+ # legacy intent in the store.
42
+ if !File.exist?(graph_path) && ActionGraphShim.shape(intent_dir) == :actions
43
+ return validate_actions_shape(intent_dir)
44
+ end
45
+
46
+ missing << "graph.md ## Graph section"
47
+ return { ok: false, missing: missing, errors: errors }
48
+ end
49
+
50
+ nodes = graph[:nodes]
51
+ edges = graph[:edges]
52
+ declared_ids = edges.keys
53
+
54
+ # Intent-scope dangling check: every node must be declared with its own
55
+ # needs-line here (unlike a roadmap, which may have undeclared roots -
56
+ # that reconciliation is G4's, not this validator's).
57
+ (nodes - declared_ids).each { |id| errors << "needs target #{id.inspect} names no declared node" }
58
+
59
+ cyc = GraphEdges.cycle(edges)
60
+ errors << "cyclic graph, cannot validate: #{cyc.join(' > ')}" if cyc
61
+
62
+ node_paths = Dir.glob(File.join(intent_dir, "nodes", "*.md")).sort
63
+ parsed_nodes = {}
64
+ paths_by_id = Hash.new { |h, k| h[k] = [] }
65
+
66
+ node_paths.each do |path|
67
+ nf = NodeFile.parse(path)
68
+ nf[:errors].each { |e| errors << "#{File.basename(path)}: #{e}" }
69
+ id = nf[:node]
70
+ next unless id
71
+
72
+ paths_by_id[id] << path
73
+ parsed_nodes[id] ||= nf
74
+ end
75
+
76
+ paths_by_id.each do |id, paths|
77
+ next unless paths.length > 1
78
+
79
+ errors << "node id #{id.inspect} claimed by more than one file: #{paths.map { |p| File.basename(p) }.join(', ')}"
80
+ end
81
+
82
+ declared_ids.each do |id|
83
+ missing << "nodes/ file for #{id}" unless parsed_nodes.key?(id)
84
+ errors << "declared node #{id.inspect} has no node file under nodes/" unless parsed_nodes.key?(id)
85
+ end
86
+ parsed_nodes.each_key do |id|
87
+ errors << "node file for #{id.inspect} exists but ## Graph declares no such node" unless declared_ids.include?(id)
88
+ end
89
+
90
+ work_nodes = []
91
+ verify_nodes = []
92
+
93
+ declared_ids.each do |id|
94
+ nf = parsed_nodes[id]
95
+ next unless nf
96
+
97
+ case nf[:kind]
98
+ when "verify"
99
+ verify_nodes << id
100
+ errors << "verify node #{id.inspect} has no ## Criteria" unless has_section?(nf[:body], "## Criteria")
101
+ when "decision"
102
+ errors << "decision node #{id.inspect} has no ## Question with a question" unless has_nonblank_section?(nf[:body], "## Question")
103
+ when "research"
104
+ errors << "research node #{id.inspect} has no ## Deposit" unless has_section?(nf[:body], "## Deposit")
105
+ when "work"
106
+ work_nodes << id
107
+ errors << "work node #{id.inspect} has no ## Steps" unless has_section?(nf[:body], "## Steps")
108
+ errors << "work node #{id.inspect} has no ## Proven by" unless has_section?(nf[:body], "## Proven by")
109
+ end
110
+ end
111
+
112
+ above_trivial_bar = work_nodes.length >= 2
113
+ if above_trivial_bar
114
+ verify_directive = parsed[:verify]
115
+ directive_ok = verify_directive && !verify_directive[:reason].to_s.empty?
116
+
117
+ unless directive_ok
118
+ attached = verify_nodes.select { |vid| node_touches_graph?(vid, edges) }
119
+ if attached.empty?
120
+ errors << "two or more work nodes require a verify node attached to the graph, or a verify: none reason=<text> directive"
121
+ end
122
+ end
123
+
124
+ work_nodes.each do |id|
125
+ nf = parsed_nodes[id]
126
+ next unless nf
127
+
128
+ errors << "work node #{id.inspect} has no failure-mode matrix under a heading carrying its id" unless has_valid_matrix?(id, nf[:body])
129
+ end
130
+ end
131
+
132
+ verify_nodes.each do |id|
133
+ next if node_touches_graph?(id, edges)
134
+
135
+ errors << "verify node #{id.inspect} exists but no edge reaches it (attach it or drop it)"
136
+ end
137
+
138
+ { ok: errors.empty?, missing: missing, errors: errors }
139
+ end
140
+
141
+ # A node "touches" the graph when some edge involves it on either side:
142
+ # it targets something, or something targets it. A verify node declared
143
+ # with "needs nothing" and targeted by nothing is fully isolated (review
144
+ # A13).
145
+ def node_touches_graph?(id, edges)
146
+ (edges[id] || []).any? || edges.values.any? { |targets| targets.include?(id) }
147
+ end
148
+
149
+ def has_section?(body, heading_text)
150
+ NodeFile.split_by_headings(body.to_s).any? { |heading, _| heading.strip == heading_text }
151
+ end
152
+
153
+ def has_nonblank_section?(body, heading_text)
154
+ NodeFile.split_by_headings(body.to_s).any? { |heading, section| heading.strip == heading_text && !section.strip.empty? }
155
+ end
156
+
157
+ def heading_tokens(heading)
158
+ heading.to_s.sub(/\A#+\s*/, "").split(/[^A-Za-z0-9]+/)
159
+ end
160
+
161
+ # The matrix table must sit under a heading whose tokens include the
162
+ # node's own id, and that heading must own at least one data row (review
163
+ # A1 - the same table-owning rule report_screen's resolver uses, so a
164
+ # node's own Proven-by cell is never "not recorded" the moment it ships).
165
+ def has_valid_matrix?(id, body)
166
+ NodeFile.split_by_headings(body.to_s).any? do |heading, section|
167
+ heading_tokens(heading).include?(id) && NodeFile.table_rows(section).any?
168
+ end
169
+ end
170
+
171
+ # The structural check the synthetic (actions/-only) shape gets, and
172
+ # nothing more (D6): at least one node, unique ids, every needs target
173
+ # names a declared node, acyclic. Never the kind-section rules, the
174
+ # failure-mode matrix bar, or the verify-attachment bar - a legacy action
175
+ # file labels its headings "S1", or nothing at all, and was never asked
176
+ # to meet a bar written for a graph authored under 327.
177
+ def validate_actions_shape(intent_dir)
178
+ errors = []
179
+ graph = ActionGraphShim.view(intent_dir)[:graph] || { nodes: [], edges: {}, errors: [] }
180
+ nodes = graph[:nodes]
181
+ edges = graph[:edges]
182
+
183
+ errors << "actions/ yields no nodes" if nodes.empty?
184
+ # D16: uniqueness is checked over the node array, not edges.keys - a
185
+ # Hash key set is unique by construction, so checking edges.keys can
186
+ # never fire.
187
+ errors << "duplicate node ids in synthetic chain" if nodes.uniq.length != nodes.length
188
+
189
+ # D16: every needs target is checked against the declared node list,
190
+ # not the node list against its own key set (nodes - edges.keys, which
191
+ # can never differ since the builder mints edges.keys from nodes).
192
+ (edges.values.flatten.uniq - nodes).each do |id|
193
+ errors << "needs target #{id.inspect} names no declared node"
194
+ end
195
+
196
+ cyc = GraphEdges.cycle(edges)
197
+ errors << "cyclic graph, cannot validate: #{cyc.join(' > ')}" if cyc
198
+
199
+ { ok: errors.empty?, missing: [], errors: errors }
200
+ end
201
+ end