@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.
- package/PLASTIC.md +13 -139
- package/README.md +345 -133
- package/agents/plastic-enforcer.md +9 -10
- package/agents/plastic-executor.md +1 -1
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +35 -1
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/verify-change +345 -0
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +4 -7
- package/{skills/auto/references → docs/help}/agent-architecture.md +7 -7
- package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
- package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/package.json +3 -2
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +10 -9
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +48 -42
- package/scripts/end-intent +2 -8
- package/scripts/file-session-intent +2 -1
- package/scripts/hook-capture +4 -3
- package/scripts/hook-close +2 -1
- package/scripts/hook-record +3 -2
- package/scripts/hook-savepoint +3 -2
- package/scripts/hook-session-start +5 -4
- package/scripts/hook-stop +2 -1
- package/scripts/insight-append +1 -2
- package/scripts/install.rb +3 -1
- package/scripts/lib/active_delivery.rb +1 -1
- package/scripts/lib/arm.rb +2 -1
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +85 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +60 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +25 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +20 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +53 -0
- package/scripts/lib/cli/commands/intent_end.rb +61 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +25 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +43 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +49 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +42 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +72 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +25 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +34 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +46 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +84 -0
- package/scripts/lib/cli/legacy.rb +50 -0
- package/scripts/lib/cli/output.rb +102 -0
- package/scripts/lib/cli/scope.rb +127 -0
- package/scripts/lib/cli/table.rb +64 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/compact_instructions.rb +8 -0
- package/scripts/lib/day_summary.rb +4 -3
- package/scripts/lib/doctor_core.rb +7 -32
- package/scripts/lib/doctor_session_ledger.rb +2 -1
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_measure_models.rb +3 -1
- package/scripts/lib/index_entry.rb +9 -0
- package/scripts/lib/installer_core.rb +73 -19
- package/scripts/lib/intent_screen.rb +3 -3
- package/scripts/lib/lock.rb +2 -2
- package/scripts/lib/node_input.rb +3 -2
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +2 -1
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +2 -0
- package/scripts/lib/report_screen.rb +4 -3
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_queue.rb +2 -2
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +3 -2
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_git.rb +4 -3
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/verify_intent.rb +2 -7
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/worktree.rb +3 -8
- package/scripts/lib/worktree_sweep.rb +3 -2
- package/scripts/link-suggest +2 -1
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +3 -12
- package/scripts/plastic-lock +3 -2
- package/scripts/promote-session-item +3 -2
- package/scripts/release-check +10 -5
- package/scripts/report-screen +1 -1
- package/scripts/session-commit +2 -1
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +6 -6
- package/templates/render.css +10 -0
- package/bin/plastic.js +0 -70
- package/skills/agent-advisor/SKILL.md +0 -84
- package/skills/auto/SKILL.md +0 -297
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -64
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -180
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -305
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -215
- package/skills/intent-continuing/SKILL.md +0 -156
- package/skills/intent-continuing/references/board-fill.md +0 -52
- package/skills/intent-continuing/references/boarding-matrix.md +0 -35
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -89
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -182
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -87
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -47
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -136
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -376
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -70
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -90
- package/skills/roadmap/references/file-format.md +0 -134
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -66
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/knowledge-graph.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
- /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
- /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",
|
|
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
|
|
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 =
|
|
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")
|
package/scripts/lib/qmd_sync.rb
CHANGED
|
@@ -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(
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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,
|
|
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 :
|
|
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,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
|
|
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,
|
|
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,
|
|
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(
|
|
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 ||
|
|
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 =
|
|
43
|
+
global_store = Plastic::StoreLayout.global_store(plastic_home)
|
|
43
44
|
if File.directory?(global_store)
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|