@zalom/plastic 2.0.0-alpha.27 → 2.0.0-alpha.29

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 (207) hide show
  1. package/PLASTIC.md +13 -139
  2. package/README.md +345 -133
  3. package/agents/plastic-enforcer.md +9 -10
  4. package/agents/plastic-executor.md +1 -1
  5. package/bin/crap +4 -0
  6. package/bin/lib/context_budget.rb +35 -1
  7. package/bin/lib/skill_census.rb +839 -0
  8. package/bin/plastic +6 -0
  9. package/bin/plastic-skill-census +114 -0
  10. package/bin/verify-change +345 -0
  11. package/deprecations.yml +1 -1
  12. package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +4 -7
  13. package/{skills/auto/references → docs/help}/agent-architecture.md +7 -7
  14. package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
  15. package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
  16. package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
  17. package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
  18. package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
  19. package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
  20. package/package.json +3 -2
  21. package/scripts/append-ledger +2 -1
  22. package/scripts/dashboard.rb +10 -9
  23. package/scripts/day-summary +2 -1
  24. package/scripts/doctor.rb +48 -42
  25. package/scripts/end-intent +2 -8
  26. package/scripts/file-session-intent +2 -1
  27. package/scripts/hook-capture +4 -3
  28. package/scripts/hook-close +2 -1
  29. package/scripts/hook-record +3 -2
  30. package/scripts/hook-savepoint +3 -2
  31. package/scripts/hook-session-start +5 -4
  32. package/scripts/hook-stop +2 -1
  33. package/scripts/insight-append +1 -2
  34. package/scripts/install.rb +3 -1
  35. package/scripts/lib/active_delivery.rb +1 -1
  36. package/scripts/lib/arm.rb +2 -1
  37. package/scripts/lib/backup.rb +65 -0
  38. package/scripts/lib/cli/command.rb +85 -0
  39. package/scripts/lib/cli/commands/auto.rb +18 -0
  40. package/scripts/lib/cli/commands/auto_brief.rb +44 -0
  41. package/scripts/lib/cli/commands/auto_lock.rb +60 -0
  42. package/scripts/lib/cli/commands/auto_report.rb +50 -0
  43. package/scripts/lib/cli/commands/auto_take.rb +25 -0
  44. package/scripts/lib/cli/commands/backup.rb +43 -0
  45. package/scripts/lib/cli/commands/checkout.rb +25 -0
  46. package/scripts/lib/cli/commands/continue.rb +66 -0
  47. package/scripts/lib/cli/commands/doctor.rb +20 -0
  48. package/scripts/lib/cli/commands/feedback.rb +40 -0
  49. package/scripts/lib/cli/commands/help.rb +69 -0
  50. package/scripts/lib/cli/commands/hook.rb +32 -0
  51. package/scripts/lib/cli/commands/index.rb +23 -0
  52. package/scripts/lib/cli/commands/install.rb +21 -0
  53. package/scripts/lib/cli/commands/installer_verb.rb +37 -0
  54. package/scripts/lib/cli/commands/intent.rb +19 -0
  55. package/scripts/lib/cli/commands/intent_answer.rb +37 -0
  56. package/scripts/lib/cli/commands/intent_command.rb +53 -0
  57. package/scripts/lib/cli/commands/intent_end.rb +61 -0
  58. package/scripts/lib/cli/commands/intent_new.rb +62 -0
  59. package/scripts/lib/cli/commands/intent_note.rb +43 -0
  60. package/scripts/lib/cli/commands/intent_rule.rb +36 -0
  61. package/scripts/lib/cli/commands/intent_show.rb +25 -0
  62. package/scripts/lib/cli/commands/intent_spec.rb +44 -0
  63. package/scripts/lib/cli/commands/intent_step.rb +43 -0
  64. package/scripts/lib/cli/commands/intent_verify.rb +26 -0
  65. package/scripts/lib/cli/commands/migrate.rb +16 -0
  66. package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
  67. package/scripts/lib/cli/commands/next.rb +49 -0
  68. package/scripts/lib/cli/commands/project.rb +19 -0
  69. package/scripts/lib/cli/commands/project_links.rb +42 -0
  70. package/scripts/lib/cli/commands/project_list.rb +20 -0
  71. package/scripts/lib/cli/commands/project_new.rb +72 -0
  72. package/scripts/lib/cli/commands/query.rb +31 -0
  73. package/scripts/lib/cli/commands/render.rb +25 -0
  74. package/scripts/lib/cli/commands/roadmap.rb +19 -0
  75. package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
  76. package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
  77. package/scripts/lib/cli/commands/roadmap_next.rb +34 -0
  78. package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
  79. package/scripts/lib/cli/commands/rollback.rb +20 -0
  80. package/scripts/lib/cli/commands/search.rb +60 -0
  81. package/scripts/lib/cli/commands/session.rb +18 -0
  82. package/scripts/lib/cli/commands/session_commit.rb +42 -0
  83. package/scripts/lib/cli/commands/session_handoff.rb +34 -0
  84. package/scripts/lib/cli/commands/session_summary.rb +35 -0
  85. package/scripts/lib/cli/commands/status.rb +68 -0
  86. package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
  87. package/scripts/lib/cli/commands/sync.rb +46 -0
  88. package/scripts/lib/cli/commands/uninstall.rb +20 -0
  89. package/scripts/lib/cli/commands/update.rb +20 -0
  90. package/scripts/lib/cli/commands/version.rb +53 -0
  91. package/scripts/lib/cli/frontier.rb +84 -0
  92. package/scripts/lib/cli/legacy.rb +50 -0
  93. package/scripts/lib/cli/output.rb +102 -0
  94. package/scripts/lib/cli/scope.rb +127 -0
  95. package/scripts/lib/cli/table.rb +64 -0
  96. package/scripts/lib/cli.rb +94 -0
  97. package/scripts/lib/compact_instructions.rb +8 -0
  98. package/scripts/lib/day_summary.rb +4 -3
  99. package/scripts/lib/doctor_core.rb +7 -32
  100. package/scripts/lib/doctor_session_ledger.rb +2 -1
  101. package/scripts/lib/feedback_report.rb +1 -1
  102. package/scripts/lib/graph_measure_models.rb +3 -1
  103. package/scripts/lib/index_entry.rb +9 -0
  104. package/scripts/lib/installer_core.rb +73 -19
  105. package/scripts/lib/intent_screen.rb +3 -3
  106. package/scripts/lib/lock.rb +2 -2
  107. package/scripts/lib/node_input.rb +3 -2
  108. package/scripts/lib/preflight.rb +4 -6
  109. package/scripts/lib/project_config.rb +2 -1
  110. package/scripts/lib/project_validator.rb +3 -2
  111. package/scripts/lib/qmd_sync.rb +8 -7
  112. package/scripts/lib/reference_archive.rb +45 -0
  113. package/scripts/lib/release_guard.rb +2 -0
  114. package/scripts/lib/report_screen.rb +4 -3
  115. package/scripts/lib/rlm/corpus.rb +13 -0
  116. package/scripts/lib/rlm/probe.rb +29 -0
  117. package/scripts/lib/rlm/query.rb +22 -0
  118. package/scripts/lib/roadmap_queue.rb +2 -2
  119. package/scripts/lib/roadmap_savepoint.rb +1 -1
  120. package/scripts/lib/runner_absorb.rb +3 -2
  121. package/scripts/lib/search_index.rb +55 -0
  122. package/scripts/lib/session_git.rb +4 -3
  123. package/scripts/lib/sqlite.rb +22 -0
  124. package/scripts/lib/store_discovery.rb +7 -6
  125. package/scripts/lib/store_layout.rb +54 -0
  126. package/scripts/lib/store_provisioning.rb +2 -1
  127. package/scripts/lib/store_sync.rb +85 -0
  128. package/scripts/lib/stores_move.rb +93 -0
  129. package/scripts/lib/verify_intent.rb +2 -7
  130. package/scripts/lib/version_number.rb +48 -0
  131. package/scripts/lib/work_graph.rb +59 -0
  132. package/scripts/lib/worktree.rb +3 -8
  133. package/scripts/lib/worktree_sweep.rb +3 -2
  134. package/scripts/link-suggest +2 -1
  135. package/scripts/migrate-to-global +1 -1
  136. package/scripts/new-intent +3 -12
  137. package/scripts/plastic-lock +3 -2
  138. package/scripts/promote-session-item +3 -2
  139. package/scripts/release-check +10 -5
  140. package/scripts/report-screen +1 -1
  141. package/scripts/session-commit +2 -1
  142. package/scripts/spawn-preamble +2 -2
  143. package/scripts/update.rb +25 -4
  144. package/scripts/write-handoff +2 -1
  145. package/templates/agents.md +6 -6
  146. package/templates/render.css +10 -0
  147. package/bin/plastic.js +0 -70
  148. package/skills/agent-advisor/SKILL.md +0 -84
  149. package/skills/auto/SKILL.md +0 -297
  150. package/skills/auto/evals/evals.json +0 -255
  151. package/skills/auto/references/end-tail.md +0 -64
  152. package/skills/conventions/SKILL.md +0 -29
  153. package/skills/dashboard/SKILL.md +0 -180
  154. package/skills/dashboard/evals/evals.json +0 -38
  155. package/skills/dashboard/references/classification.md +0 -22
  156. package/skills/dashboard/templates/dashboard-global.md +0 -20
  157. package/skills/dashboard/templates/dashboard-project.md +0 -19
  158. package/skills/direct/SKILL.md +0 -66
  159. package/skills/direct/references/request-signals.md +0 -59
  160. package/skills/doctor/SKILL.md +0 -305
  161. package/skills/doctor/report.md +0 -102
  162. package/skills/feedback/SKILL.md +0 -98
  163. package/skills/feedback/references/transport-and-privacy.md +0 -65
  164. package/skills/feedback/report.md +0 -36
  165. package/skills/install/SKILL.md +0 -215
  166. package/skills/intent-continuing/SKILL.md +0 -156
  167. package/skills/intent-continuing/references/board-fill.md +0 -52
  168. package/skills/intent-continuing/references/boarding-matrix.md +0 -35
  169. package/skills/intent-continuing/references/context-management.md +0 -28
  170. package/skills/intent-continuing/references/liveness-ranking.md +0 -57
  171. package/skills/intent-creating/SKILL.md +0 -89
  172. package/skills/intent-creating/evals/evals.json +0 -72
  173. package/skills/intent-creating/references/lifecycle.md +0 -81
  174. package/skills/intent-creating/references/wikilinks.md +0 -8
  175. package/skills/intent-ending/SKILL.md +0 -182
  176. package/skills/intent-ending/evals/evals.json +0 -74
  177. package/skills/intent-executing/SKILL.md +0 -87
  178. package/skills/intent-executing/evals/evals.json +0 -66
  179. package/skills/intent-executing/implementer-prompt.md +0 -47
  180. package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
  181. package/skills/intent-speccing/SKILL.md +0 -136
  182. package/skills/intent-speccing/evals/evals.json +0 -126
  183. package/skills/intent-speccing/references/design-principles.md +0 -44
  184. package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
  185. package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
  186. package/skills/project-creating/SKILL.md +0 -162
  187. package/skills/project-creating/references/hubs-projects.md +0 -55
  188. package/skills/project-creating/references/project-scaffolding.md +0 -97
  189. package/skills/releasing/SKILL.md +0 -376
  190. package/skills/releasing/references/deprecations.md +0 -60
  191. package/skills/releasing/references/promotion-and-tagging.md +0 -70
  192. package/skills/releasing/references/release-lines.md +0 -105
  193. package/skills/roadmap/SKILL.md +0 -90
  194. package/skills/roadmap/references/file-format.md +0 -134
  195. package/skills/roadmap/references/operations.md +0 -112
  196. package/skills/rollback/SKILL.md +0 -91
  197. package/skills/tutorial/SKILL.md +0 -66
  198. package/skills/tutorial/evals/evals.json +0 -186
  199. package/skills/uninstall/SKILL.md +0 -75
  200. package/skills/update/SKILL.md +0 -126
  201. /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
  202. /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
  203. /package/{skills/conventions/references → docs/help}/knowledge-graph.md +0 -0
  204. /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
  205. /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
  206. /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
  207. /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
@@ -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 "yaml"
5
6
 
6
7
  # ProjectConfig - reads a project's project.yml merged over the shipped defaults
@@ -18,7 +19,7 @@ module ProjectConfig
18
19
  }.freeze
19
20
 
20
21
  def read(slug)
21
- path = File.join(Dir.home, ".plastic", "projects", slug, "project.yml")
22
+ path = File.join(Plastic::StoreLayout.project_root(File.join(Dir.home, ".plastic"), slug), "project.yml")
22
23
  config = if File.exist?(path)
23
24
  YAML.safe_load(File.read(path)) || {}
24
25
  else
@@ -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 "yaml"
5
6
 
6
7
  # ProjectValidator - the single source of truth for "is a project spawn
@@ -10,7 +11,7 @@ require "yaml"
10
11
  # missing the pieces a real project needs: project.yml, a root AGENTS.md.
11
12
  # The intent-26 spawn shipped exactly that shape and was caught only by a
12
13
  # much later, pull-only plastic-doctor sweep. This module lets
13
- # plastic-project-creating verify a spawn BEFORE announcing it as done,
14
+ # `plastic project new` verify a spawn BEFORE announcing it as done,
14
15
  # mirroring how scripts/new-intent already runs IntentValidator before
15
16
  # announcing a new intent (validate-intent).
16
17
  #
@@ -52,7 +53,7 @@ module ProjectValidator
52
53
  errors << "registered project directory does not exist: #{project_path}"
53
54
  end
54
55
 
55
- project_dir = File.join(plastic_home, "projects", slug)
56
+ project_dir = Plastic::StoreLayout.project_root(plastic_home, slug)
56
57
 
57
58
  # Invariant 3: project.yml exists AND parses as YAML.
58
59
  project_yml_path = File.join(project_dir, "project.yml")
@@ -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 "yaml"
5
6
  require "json"
6
7
 
@@ -50,7 +51,7 @@ module QmdSync
50
51
  # to the directory's parent name when no registry match exists.
51
52
  def collection_name(store_dir, plastic_home:)
52
53
  store_dir = File.expand_path(store_dir)
53
- global_store = File.expand_path(File.join(plastic_home, "store"))
54
+ global_store = File.expand_path(Plastic::StoreLayout.global_store(plastic_home))
54
55
  return "plastic-global" if store_dir == global_store
55
56
 
56
57
  if File.basename(store_dir) == "roadmaps"
@@ -69,7 +70,7 @@ module QmdSync
69
70
  def enumerate_stores(plastic_home:)
70
71
  stores = [{
71
72
  collection: "plastic-global",
72
- dir: File.expand_path(File.join(plastic_home, "store")),
73
+ dir: File.expand_path(Plastic::StoreLayout.global_store(plastic_home)),
73
74
  }]
74
75
 
75
76
  projects = load_projects(plastic_home)
@@ -80,7 +81,7 @@ module QmdSync
80
81
  # Project stores live under ~/.plastic/projects/<slug>/store as the mirror;
81
82
  # registry `path` is the project code dir, so the tactical store is the
82
83
  # plastic_home projects mirror.
83
- mirror_store = File.expand_path(File.join(plastic_home, "projects", slug.to_s, "store"))
84
+ mirror_store = File.expand_path(File.join(Plastic::StoreLayout.project_root(plastic_home, slug.to_s), "store"))
84
85
  dir = Dir.exist?(mirror_store) ? mirror_store : project_store
85
86
  stores << { collection: "plastic-#{slug}", dir: dir }
86
87
  end
@@ -245,13 +246,13 @@ module QmdSync
245
246
  path = info.is_a?(Hash) ? info["path"] : nil
246
247
  next unless path
247
248
  project_root = File.expand_path(path)
248
- mirror = File.expand_path(File.join(plastic_home, "projects", slug.to_s, "store"))
249
+ mirror = File.expand_path(File.join(Plastic::StoreLayout.project_root(plastic_home, slug.to_s), "store"))
249
250
  return slug.to_s if store_dir == File.join(project_root, "store") || store_dir == mirror
250
251
  end
251
252
  # Fallback: <...>/projects/<slug>/store -> slug, else parent dir name.
252
- parts = store_dir.split(File::SEPARATOR)
253
- idx = parts.rindex("projects")
254
- return parts[idx + 1] if idx && parts[idx + 1] && parts[idx + 2] == "store"
253
+ slug = Plastic::StoreLayout.locate(store_dir).last
254
+ return slug unless slug == Plastic::StoreLayout::GLOBAL
255
+
255
256
  File.basename(File.dirname(store_dir))
256
257
  end
257
258
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require_relative "sqlite"
5
+
6
+ module Plastic
7
+ module ReferenceArchive
8
+ NAME = "references.db"
9
+ SCHEMA = <<~SQL
10
+ CREATE TABLE IF NOT EXISTS sqlar(name TEXT PRIMARY KEY, mode INT, mtime INT, sz INT, data BLOB, intent_id TEXT, sha256 TEXT);
11
+ SQL
12
+ INTENT_ID = %r{/([^/-]+)--[^/]+/}
13
+ SKIPPED = %r{(\A|/)(\.git|\.tmp)/|\.DS_Store\z|\.lock\z}
14
+
15
+ def self.path(home)
16
+ File.join(home, NAME)
17
+ end
18
+
19
+ def self.files(home)
20
+ Dir.glob("{store,projects,stores}/**/*", File::FNM_DOTMATCH, base: home)
21
+ .select { |file| File.file?(File.join(home, file)) && File.extname(file) != ".md" && !file.match?(SKIPPED) }.sort
22
+ end
23
+
24
+ def self.build(home)
25
+ known = Sqlite.call(path(home), "#{SCHEMA}SELECT name, mtime, sz FROM sqlar;").to_h { |row| [row["name"], row.values_at("mtime", "sz")] }
26
+ changed = files(home).reject { |file| known[file] == stamp(File.join(home, file)) }
27
+ Sqlite.call(path(home), ["BEGIN;", *changed.map { |file| insert(home, file) }, "COMMIT;"].join("\n"))
28
+ changed.size
29
+ end
30
+
31
+ def self.stamp(source)
32
+ stat = File.stat(source)
33
+ [stat.mtime.to_i, stat.size]
34
+ end
35
+
36
+ def self.insert(home, file)
37
+ source = File.join(home, file)
38
+ stat = File.stat(source)
39
+ id = file[INTENT_ID, 1]
40
+ read = "readfile(#{Sqlite.quote(source)})"
41
+ "INSERT OR REPLACE INTO sqlar VALUES(#{Sqlite.quote(file)}, #{stat.mode}, #{stat.mtime.to_i}, length(#{read}), " \
42
+ "sqlar_compress(#{read}), #{id ? Sqlite.quote(id) : "NULL"}, #{Sqlite.quote(Digest::SHA256.file(source).hexdigest)});"
43
+ end
44
+ end
45
+ end
@@ -52,6 +52,8 @@ module ReleaseGuard
52
52
  # to the stable channel. Pure function, no ENV reads, shared by
53
53
  # scripts/release-check and the test suite so the channel rule has exactly
54
54
  # one implementation (intent 347).
55
+ CHANNELS = {"alpha" => "alpha", "beta" => "beta", "main" => "latest"}.freeze
56
+
55
57
  def self.dist_tag(version)
56
58
  suffix = version[/-(.+)\z/, 1]
57
59
  return "latest" if suffix.nil?
@@ -8,6 +8,7 @@
8
8
  # out. Dependency injection for anything reaching outside the fixture: the
9
9
  # clock is passed as `now:`, git tag reading as `tag_reader:`, and the ANSI
10
10
  # renderer path as `renderer_path:` (D2).
11
+ require_relative "store_layout"
11
12
  require "time"
12
13
  require "json"
13
14
  require "date"
@@ -805,14 +806,14 @@ def self.matching_action_heading(intent_dir, label)
805
806
  # (<home>/projects/<slug>/store/<id--slug>); nil otherwise (a global-store
806
807
  # intent, a project with no `flow:` key, or malformed YAML). Pure: no git,
807
808
  # no shell-out, just the one file this intent's own layout already reads.
808
- PROJECT_LAYOUT_RE = %r{\A(.*)/projects/([^/]+)/store/[^/]+\z}.freeze
809
+ PROJECT_LAYOUT_RE = %r{\A(.*)/(?:projects|stores)/([^/]+)/store/[^/]+\z}.freeze
809
810
 
810
811
  def self.flow_base(intent_dir)
811
812
  m = intent_dir.to_s.match(PROJECT_LAYOUT_RE)
812
813
  return nil unless m
813
814
 
814
815
  home, slug = m[1], m[2]
815
- path = File.join(home, "projects", slug, "project.yml")
816
+ path = File.join(Plastic::StoreLayout.project_root(home, slug), "project.yml")
816
817
  return nil unless File.exist?(path)
817
818
 
818
819
  require "yaml"
@@ -1480,7 +1481,7 @@ def self.matching_action_heading(intent_dir, label)
1480
1481
 
1481
1482
  # D5: "global" is <home> itself; any other slug is <home>/projects/<slug>.
1482
1483
  def self.store_for_slug(home, slug)
1483
- slug == "global" ? home : File.join(home, "projects", slug)
1484
+ slug == "global" ? home : Plastic::StoreLayout.project_root(home, slug)
1484
1485
  end
1485
1486
 
1486
1487
  # D4: the newest valid day directory that is not in the future, when
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RLM
4
+ class Corpus
5
+ def initialize(search)
6
+ @search = search
7
+ end
8
+
9
+ def call(terms, limit)
10
+ @search.call(terms, limit)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "corpus"
4
+
5
+ module RLM
6
+ class Probe
7
+ STOP_WORDS = %w[a an and are as at be by do does for from how in is it of on or that the this to was what when where which who why with].freeze
8
+ DEFAULT_LIMIT = 10
9
+
10
+ def initialize(corpus, limit: DEFAULT_LIMIT)
11
+ @corpus = corpus
12
+ @limit = limit
13
+ end
14
+
15
+ def call(question)
16
+ hits = terms(question).flat_map { |term| @corpus.call([term], @limit) }
17
+ hits.group_by { |row| row["id"] }.values
18
+ .map { |rows| rows.first.merge("score" => rows.size) }
19
+ .sort_by.with_index { |row, index| [-row["score"], index] }
20
+ .first(@limit)
21
+ end
22
+
23
+ private
24
+
25
+ def terms(question)
26
+ question.downcase.scan(/[[:alnum:]]+/).uniq - STOP_WORDS
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "probe"
4
+
5
+ module RLM
6
+ class Query
7
+ DEFAULT_LIMIT = 3
8
+
9
+ def initialize(probe, judge: nil, limit: DEFAULT_LIMIT)
10
+ @probe = probe
11
+ @judge = judge
12
+ @limit = limit
13
+ end
14
+
15
+ def call(question)
16
+ rows = @probe.call(question)
17
+ return rows unless @judge && rows.size > @limit
18
+
19
+ @judge.call(question, rows)
20
+ end
21
+ end
22
+ end
@@ -411,8 +411,8 @@ class RoadmapQueue
411
411
  # --- scope + payload -----------------------------------------------------------
412
412
 
413
413
  def scope_label
414
- m = @roadmaps_dir.to_s.match(%r{/projects/([^/]+)/roadmaps/?\z})
415
- m ? "project:#{m[1]}" : "global"
414
+ m = @roadmaps_dir.to_s.match(%r{/(?:projects|stores)/([^/]+)/roadmaps/?\z})
415
+ (m && m[1] != "global") ? "project:#{m[1]}" : "global"
416
416
  end
417
417
 
418
418
  def payload(mode:, state:, roadmap:, frontier_wave:, dispatchable:, in_flight:, blocked:, tie:, tie_candidates:)
@@ -224,7 +224,7 @@ module RoadmapSavepoint
224
224
 
225
225
  # The tier root is the parent of `roadmaps/` (a live roadmap's grandparent, or, for one
226
226
  # already moved to `roadmaps/archived/`, its great-grandparent); INDEX.md is that root's
227
- # sibling file, matching the layout `plastic-roadmap`'s file-format doc defines.
227
+ # sibling file, matching the layout docs/help/roadmaps.md defines.
228
228
  def index_path_for(roadmap_path)
229
229
  dir = File.dirname(roadmap_path)
230
230
  dir = File.dirname(dir) if File.basename(dir) == "archived"
@@ -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 "yaml"
5
6
  require "date"
6
7
  require "time"
@@ -59,7 +60,7 @@ module RunnerAbsorb
59
60
  # to plain strings), this cap is purely about line length.
60
61
  FINDING_CAP = 200
61
62
 
62
- PROJECT_LAYOUT_RE = %r{\A(.*)/projects/([^/]+)/store/[^/]+\z}.freeze
63
+ PROJECT_LAYOUT_RE = %r{\A(.*)/(?:projects|stores)/([^/]+)/store/[^/]+\z}.freeze
63
64
 
64
65
  # absorb(context, node:, return_path:) -> a result hash describing the one
65
66
  # transition it wrote (or attempted to write). Never raises across its own
@@ -675,7 +676,7 @@ module RunnerAbsorb
675
676
  return nil unless m
676
677
 
677
678
  home, slug = m[1], m[2]
678
- path = File.join(home, "projects", slug, "project.yml")
679
+ path = File.join(Plastic::StoreLayout.project_root(home, slug), "project.yml")
679
680
  return nil unless File.exist?(path)
680
681
 
681
682
  data = begin
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "sqlite"
5
+
6
+ module Plastic
7
+ module SearchIndex
8
+ NAME = "knowledge_graph.db"
9
+ SCHEMA = <<~SQL
10
+ CREATE TABLE doc(id INTEGER PRIMARY KEY, path TEXT, sz INT, data BLOB, hash TEXT);
11
+ CREATE VIEW doc_v AS SELECT id AS rowid, CAST(sqlar_uncompress(data, sz) AS TEXT) AS body FROM doc;
12
+ CREATE VIRTUAL TABLE ft USING fts5(body, content='doc_v', content_rowid='rowid');
13
+ SQL
14
+ FINISH = "INSERT INTO ft(ft) VALUES('rebuild'); INSERT INTO ft(ft) VALUES('optimize'); VACUUM;"
15
+
16
+ def self.path(home)
17
+ File.join(home, NAME)
18
+ end
19
+
20
+ def self.files(home)
21
+ Dir.glob("{store,projects,stores}/**/*.md", File::FNM_DOTMATCH, base: home).reject { |file| file.include?("/.git/") }.sort
22
+ end
23
+
24
+ def self.build(home)
25
+ files = files(home)
26
+ draft = "#{path(home)}.tmp"
27
+ FileUtils.rm_f(draft)
28
+ Sqlite.call(draft, [SCHEMA, "BEGIN;", *files.map { |file| insert(home, file) }, "COMMIT;", FINISH].join("\n"))
29
+ File.rename(draft, path(home))
30
+ files.size
31
+ ensure
32
+ FileUtils.rm_f(draft.to_s)
33
+ end
34
+
35
+ def self.insert(home, file)
36
+ source = quote(File.join(home, file))
37
+ "INSERT INTO doc(path, sz, data, hash) VALUES(#{quote(file)}, length(readfile(#{source})), " \
38
+ "sqlar_compress(readfile(#{source})), lower(hex(sha3(readfile(#{source}), 256))));"
39
+ end
40
+
41
+ def self.search(home, terms, limit:, prefix: "")
42
+ phrases = terms.map { |term| %("#{term.delete('"')}") }.join(" ")
43
+ Sqlite.call(path(home), <<~SQL, readonly: true)
44
+ SELECT doc.id AS id, doc.path AS path, snippet(ft, 0, '[', ']', ' ... ', 12) AS excerpt
45
+ FROM ft JOIN doc ON doc.id = ft.rowid
46
+ WHERE ft MATCH #{quote(phrases)} AND doc.path LIKE #{quote("#{prefix}%")}
47
+ ORDER BY rank LIMIT #{Integer(limit)};
48
+ SQL
49
+ end
50
+
51
+ def self.quote(text)
52
+ Sqlite.quote(text)
53
+ end
54
+ end
55
+ 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 "yaml"
5
6
  require_relative "worktree"
6
7
  require_relative "scaffold_intent"
@@ -160,7 +161,7 @@ module SessionGit
160
161
  def read_project_flow(slug, plastic_home)
161
162
  return nil if slug == "global"
162
163
 
163
- project_yml = File.join(plastic_home, "projects", slug, "project.yml")
164
+ project_yml = File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "project.yml")
164
165
  return nil unless File.exist?(project_yml)
165
166
 
166
167
  data = begin
@@ -196,7 +197,7 @@ module SessionGit
196
197
  resolver ||= lambda { |short_session|
197
198
  ActiveDelivery.resolve_by_short_session(
198
199
  global_store: store,
199
- project_roots: ActiveDelivery.project_roots(File.dirname(store)),
200
+ project_roots: ActiveDelivery.project_roots(Plastic::StoreLayout.locate(store).first),
200
201
  short_session: short_session
201
202
  )
202
203
  }
@@ -333,7 +334,7 @@ module SessionGit
333
334
  # a detached HEAD. Both modes now share exactly one check of each.
334
335
  def commit!(cwd:, summary:, day:, session:, plastic_home:, store: nil,
335
336
  runner: Worktree::ShellRunner.new, gh_runner: GhRunner.new)
336
- effective_store = store || File.join(plastic_home, "store")
337
+ effective_store = store || Plastic::StoreLayout.global_store(plastic_home)
337
338
  repo = resolve_repo(cwd, runner: runner)
338
339
  return note("no repo") if repo.nil?
339
340
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+
6
+ module Plastic
7
+ module Sqlite
8
+ Error = Class.new(StandardError)
9
+
10
+ def self.quote(text)
11
+ "'#{text.gsub("'", "''")}'"
12
+ end
13
+
14
+ def self.call(path, sql, readonly: false)
15
+ flags = readonly ? ["-readonly"] : []
16
+ out, err, status = Open3.capture3("sqlite3", "-json", *flags, path, stdin_data: sql)
17
+ raise Error, err.strip unless status.success?
18
+
19
+ out.strip.empty? ? [] : JSON.parse(out)
20
+ end
21
+ end
22
+ 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 "store_provisioning"
5
6
 
6
7
  # StoreDiscovery: the single source of truth for "what stores exist" (intent 189).
@@ -39,20 +40,20 @@ module StoreDiscovery
39
40
  stores = []
40
41
  missing = []
41
42
 
42
- global_store = File.join(plastic_home, "store")
43
+ global_store = Plastic::StoreLayout.global_store(plastic_home)
43
44
  if File.directory?(global_store)
44
- stores << { key: "global", slug: "global", root: plastic_home,
45
- store: global_store, index: File.join(plastic_home, "INDEX.md") }
45
+ global_root = Plastic::StoreLayout.global_root(plastic_home)
46
+ stores << { key: "global", slug: "global", root: global_root,
47
+ store: global_store, index: File.join(global_root, "INDEX.md") }
46
48
  end
47
49
 
48
50
  registered = StoreProvisioning.load_projects(plastic_home) # { slug => info }, {} on error/absence
49
- projects_root = File.join(plastic_home, "projects")
50
- on_disk = File.directory?(projects_root) ? Dir.children(projects_root).reject { |e| e.start_with?(".") } : []
51
+ on_disk = Plastic::StoreLayout.project_slugs(plastic_home)
51
52
 
52
53
  all_slugs = (registered.keys + on_disk).uniq.sort
53
54
 
54
55
  all_slugs.each do |slug|
55
- root = File.join(projects_root, slug)
56
+ root = Plastic::StoreLayout.project_root(plastic_home, slug)
56
57
  store_dir = File.join(root, "store")
57
58
  if File.directory?(store_dir)
58
59
  stores << { key: "project:#{slug}", slug: slug, root: root,
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plastic
4
+ module StoreLayout
5
+ GLOBAL = "global"
6
+ PARENTS = %w[stores projects].freeze
7
+
8
+ module_function
9
+
10
+ def moved?(home)
11
+ File.directory?(File.join(home, "stores"))
12
+ end
13
+
14
+ def global_root(home)
15
+ moved?(home) ? File.join(home, "stores", GLOBAL) : home
16
+ end
17
+
18
+ def global_store(home)
19
+ File.join(global_root(home), "store")
20
+ end
21
+
22
+ def projects_root(home)
23
+ File.join(home, moved?(home) ? "stores" : "projects")
24
+ end
25
+
26
+ def project_root(home, slug)
27
+ File.join(projects_root(home), slug)
28
+ end
29
+
30
+ def root(home, slug)
31
+ (slug == GLOBAL) ? global_root(home) : project_root(home, slug)
32
+ end
33
+
34
+ def project_slugs(home)
35
+ parent = projects_root(home)
36
+ return [] unless File.directory?(parent)
37
+
38
+ Dir.children(parent).reject { |entry| entry.start_with?(".") || entry == GLOBAL }.sort
39
+ end
40
+
41
+ def locate(store)
42
+ root = File.dirname(File.expand_path(store))
43
+ parent = File.dirname(root)
44
+ return [root, GLOBAL] unless PARENTS.include?(File.basename(parent))
45
+
46
+ [File.dirname(parent), File.basename(root)]
47
+ end
48
+
49
+ def home_and_scope(store)
50
+ home, slug = locate(store)
51
+ [home, (slug == GLOBAL) ? GLOBAL : "project:#{slug}"]
52
+ end
53
+ end
54
+ 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 "fileutils"
5
6
  require "yaml"
6
7
 
@@ -63,7 +64,7 @@ module StoreProvisioning
63
64
  }
64
65
  end
65
66
 
66
- project_dir = File.join(plastic_home, "projects", slug)
67
+ project_dir = Plastic::StoreLayout.project_root(plastic_home, slug)
67
68
  store_dir = File.join(project_dir, "store")
68
69
  FileUtils.mkdir_p(store_dir)
69
70
 
@@ -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