feature_machine 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE +21 -0
- data/README.md +207 -0
- data/app/assets/feature_machine/default.css +62 -0
- data/app/controllers/api/feature_machine/base_controller.rb +109 -0
- data/app/controllers/api/feature_machine/bug_reports_controller.rb +32 -0
- data/app/controllers/api/feature_machine/feature_requests_controller.rb +31 -0
- data/app/controllers/api/feature_machine/maintainer/feature_requests_controller.rb +28 -0
- data/app/controllers/api/feature_machine/roadmaps_controller.rb +9 -0
- data/app/controllers/api/feature_machine/work_items_controller.rb +56 -0
- data/app/controllers/concerns/feature_machine/current_user.rb +28 -0
- data/app/controllers/concerns/feature_machine/engine_layout.rb +12 -0
- data/app/controllers/feature_machine/admin/attachments_controller.rb +17 -0
- data/app/controllers/feature_machine/admin/base_controller.rb +16 -0
- data/app/controllers/feature_machine/admin/feature_requests_controller.rb +24 -0
- data/app/controllers/feature_machine/admin/triage_controller.rb +9 -0
- data/app/controllers/feature_machine/admin/work_item_relations_controller.rb +24 -0
- data/app/controllers/feature_machine/admin/work_items_controller.rb +88 -0
- data/app/controllers/feature_machine/base_controller.rb +10 -0
- data/app/controllers/feature_machine/bug_reports_controller.rb +29 -0
- data/app/controllers/feature_machine/feature_requests_controller.rb +27 -0
- data/app/controllers/feature_machine/mcp_controller.rb +36 -0
- data/app/controllers/feature_machine/roadmap_controller.rb +9 -0
- data/app/helpers/feature_machine/feature_requests_helper.rb +90 -0
- data/app/helpers/feature_machine/work_items_helper.rb +99 -0
- data/app/jobs/feature_machine/application_job.rb +6 -0
- data/app/jobs/feature_machine/request_embedding_job.rb +12 -0
- data/app/jobs/feature_machine/work_item_embedding_job.rb +12 -0
- data/app/models/feature_machine/request.rb +111 -0
- data/app/models/feature_machine/roadmap.rb +24 -0
- data/app/models/feature_machine/work_item.rb +203 -0
- data/app/models/feature_machine/work_item_relation.rb +60 -0
- data/app/prompts/feature_machine/drafter/_product_blurb.txt.erb +1 -0
- data/app/prompts/feature_machine/drafter/instructions.txt.erb +7 -0
- data/app/services/feature_machine/base_embedding_service.rb +52 -0
- data/app/services/feature_machine/drafter.rb +43 -0
- data/app/services/feature_machine/judge.rb +53 -0
- data/app/services/feature_machine/request_embedding_service.rb +14 -0
- data/app/services/feature_machine/triage_buckets.rb +146 -0
- data/app/services/feature_machine/work_item_drafter.rb +91 -0
- data/app/services/feature_machine/work_item_embedding_service.rb +14 -0
- data/app/services/feature_machine/work_item_spec_schema.rb +14 -0
- data/app/tools/feature_machine/mcp/base.rb +94 -0
- data/app/tools/feature_machine/mcp/get_work_item.rb +20 -0
- data/app/tools/feature_machine/mcp/list_bug_reports.rb +20 -0
- data/app/tools/feature_machine/mcp/list_feature_requests.rb +29 -0
- data/app/tools/feature_machine/mcp/list_work_items.rb +30 -0
- data/app/tools/feature_machine/mcp/read_roadmap.rb +22 -0
- data/app/tools/feature_machine/mcp/report_bug.rb +26 -0
- data/app/tools/feature_machine/mcp/request_feature.rb +27 -0
- data/app/tools/feature_machine/mcp/update_work_item.rb +33 -0
- data/app/tools/feature_machine/mcp.rb +25 -0
- data/app/views/api/feature_machine/feature_requests/index.json.jbuilder +2 -0
- data/app/views/api/feature_machine/feature_requests/show.json.jbuilder +1 -0
- data/app/views/api/feature_machine/maintainer/feature_requests/index.json.jbuilder +1 -0
- data/app/views/api/feature_machine/roadmaps/_work_item.json.jbuilder +12 -0
- data/app/views/api/feature_machine/roadmaps/show.json.jbuilder +10 -0
- data/app/views/api/feature_machine/work_items/_work_item.json.jbuilder +32 -0
- data/app/views/api/feature_machine/work_items/index.json.jbuilder +1 -0
- data/app/views/api/feature_machine/work_items/show.json.jbuilder +3 -0
- data/app/views/feature_machine/_default_styles.html.erb +5 -0
- data/app/views/feature_machine/_flash.html.erb +10 -0
- data/app/views/feature_machine/admin/triage/show.html.erb +120 -0
- data/app/views/feature_machine/admin/work_items/_form.html.erb +78 -0
- data/app/views/feature_machine/admin/work_items/edit.html.erb +40 -0
- data/app/views/feature_machine/admin/work_items/index.html.erb +80 -0
- data/app/views/feature_machine/admin/work_items/new.html.erb +21 -0
- data/app/views/feature_machine/admin/work_items/show.html.erb +168 -0
- data/app/views/feature_machine/admin/work_items/show.md.erb +31 -0
- data/app/views/feature_machine/bug_reports/index.html.erb +38 -0
- data/app/views/feature_machine/bug_reports/new.html.erb +83 -0
- data/app/views/feature_machine/feature_requests/_feature_request.html.erb +47 -0
- data/app/views/feature_machine/feature_requests/_feature_request.json.jbuilder +15 -0
- data/app/views/feature_machine/feature_requests/index.html.erb +44 -0
- data/app/views/feature_machine/feature_requests/new.html.erb +84 -0
- data/app/views/feature_machine/roadmap/_work_item.html.erb +34 -0
- data/app/views/feature_machine/roadmap/show.html.erb +70 -0
- data/app/views/layouts/feature_machine.html.erb +21 -0
- data/db/migrate/20260910180000_create_feature_requests.rb +18 -0
- data/db/migrate/20261004120000_create_work_items.rb +52 -0
- data/db/migrate/20261006120000_add_pull_request_url_to_work_items.rb +7 -0
- data/lib/feature_machine/auth.rb +89 -0
- data/lib/feature_machine/calibration/judge_cases.yml +137 -0
- data/lib/feature_machine/configuration.rb +165 -0
- data/lib/feature_machine/engine.rb +34 -0
- data/lib/feature_machine/judge_calibration.rb +85 -0
- data/lib/feature_machine/routes.rb +61 -0
- data/lib/feature_machine/version.rb +3 -0
- data/lib/feature_machine.rb +57 -0
- data/lib/generators/feature_machine/install/install_generator.rb +76 -0
- data/lib/generators/feature_machine/install/templates/initializer.rb +99 -0
- data/lib/tasks/feature_machine_calibration.rake +17 -0
- data/lib/tasks/feature_machine_embeddings.rake +10 -0
- metadata +207 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<% product = FeatureMachine.config.product_name %>
|
|
2
|
+
<% roadmap = FeatureMachine.config.host_path(:roadmap_path, self) %>
|
|
3
|
+
<% bug_reports = FeatureMachine.config.host_path(:bug_reports_path, self) %>
|
|
4
|
+
<% button = "cn-button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 group/button select-none cn-button-size-default" %>
|
|
5
|
+
<% content_for :title, "Your feature requests — #{product}" %>
|
|
6
|
+
<% content_for :head do %>
|
|
7
|
+
<meta name="robots" content="noindex">
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<div class="cn-container">
|
|
11
|
+
<%= render "feature_machine/flash" %>
|
|
12
|
+
<header class="cn-page-header">
|
|
13
|
+
<p class="cn-page-header-eyebrow">Roadmap</p>
|
|
14
|
+
<h1 class="cn-page-header-title">Your feature requests</h1>
|
|
15
|
+
<p class="cn-page-header-lede">
|
|
16
|
+
What you've asked for and where each request stands. Only you can see
|
|
17
|
+
this page.<% if roadmap %> What requests turn into is on the <%= link_to "roadmap", roadmap %>.<% end %>
|
|
18
|
+
</p>
|
|
19
|
+
<div class="mt-5 flex flex-wrap items-center gap-3">
|
|
20
|
+
<a href="<%= new_feature_request_path %>" data-slot="button" data-variant="default" data-size="default"
|
|
21
|
+
class="<%= button %> cn-button-variant-default"><span data-slot="label" class="contents">Request a feature</span></a>
|
|
22
|
+
<% if bug_reports %>
|
|
23
|
+
<a href="<%= bug_reports %>" data-slot="button" data-variant="ghost" data-size="default"
|
|
24
|
+
class="<%= button %> cn-button-variant-ghost"><span data-slot="label" class="contents">Your bug reports</span></a>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
<% if @feature_requests.empty? %>
|
|
30
|
+
<div data-slot="empty" class="cn-empty flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance">
|
|
31
|
+
<div data-slot="empty-header" class="cn-empty-header flex max-w-sm flex-col items-center">
|
|
32
|
+
<div data-slot="empty-icon" data-variant="icon" class="cn-empty-media flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 cn-empty-media-icon">
|
|
33
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>
|
|
34
|
+
</div>
|
|
35
|
+
<h2 data-slot="empty-title" class="cn-empty-title cn-font-heading">No feature requests yet</h2>
|
|
36
|
+
<p data-slot="empty-description" class="cn-empty-description text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary">What would make <%= product %> more useful to you? Ask, and follow it here.</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<% else %>
|
|
40
|
+
<div class="cn-entry-list" id="feature-requests">
|
|
41
|
+
<%= render partial: "feature_machine/feature_requests/feature_request", collection: @feature_requests %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<% product = FeatureMachine.config.product_name %>
|
|
2
|
+
<% new_bug_report = FeatureMachine.config.host_path(:new_bug_report_path, self) %>
|
|
3
|
+
<% errors = @feature_request.errors %>
|
|
4
|
+
<% button = "cn-button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 group/button select-none cn-button-size-default" %>
|
|
5
|
+
<% field_class = "cn-field grid group/field w-full" %>
|
|
6
|
+
<% label_class = "cn-label flex items-center select-none cn-field-label group/field-label peer/field-label flex w-fit items-center" %>
|
|
7
|
+
<% hint_class = "cn-field-description [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 font-normal leading-normal" %>
|
|
8
|
+
<% content_for :title, "Request a feature — #{product}" %>
|
|
9
|
+
<% content_for :head do %>
|
|
10
|
+
<meta name="robots" content="noindex">
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<div class="cn-container">
|
|
14
|
+
<%= render "feature_machine/flash" %>
|
|
15
|
+
<header class="cn-page-header">
|
|
16
|
+
<p class="cn-page-header-eyebrow">Roadmap</p>
|
|
17
|
+
<h1 class="cn-page-header-title">Request a feature</h1>
|
|
18
|
+
<p class="cn-page-header-lede">
|
|
19
|
+
Tell us what you're trying to do. Only you and the maintainers see what
|
|
20
|
+
you write; you can follow it under
|
|
21
|
+
<%= link_to "your feature requests", feature_requests_path %>.
|
|
22
|
+
<% if new_bug_report %><%= link_to "Reporting a bug instead?", new_bug_report %><% end %>
|
|
23
|
+
</p>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<%# Plain form_with: the same Field / Input / Textarea / Button markup the
|
|
27
|
+
host's Poetry builder renders, written out so the engine carries no
|
|
28
|
+
dependency on the host's UI kit. Field order inside each cn-field:
|
|
29
|
+
label, control, hint, error. aria-required rides the label-station
|
|
30
|
+
rule (never the native attribute); aria-describedby points at the
|
|
31
|
+
hint and, on a bounce, the error. %>
|
|
32
|
+
<%= form_with(model: @feature_request, class: "flex max-w-xl flex-col gap-5") do |form| %>
|
|
33
|
+
<% if errors.any? %>
|
|
34
|
+
<div data-slot="alert" data-variant="destructive" role="alert" class="cn-alert relative w-full group/alert cn-alert-variant-destructive">
|
|
35
|
+
<div data-slot="alert-title" class="cn-alert-title [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3">That didn't go through</div>
|
|
36
|
+
<div data-slot="alert-description" class="cn-alert-description [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3">
|
|
37
|
+
<ul class="list-inside list-disc">
|
|
38
|
+
<% errors.full_messages.each do |message| %>
|
|
39
|
+
<li><%= message %></li>
|
|
40
|
+
<% end %>
|
|
41
|
+
</ul>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<% title_errors = errors[:title] %>
|
|
47
|
+
<div data-slot="field" data-invalid="<%= title_errors.any? %>" class="<%= field_class %>">
|
|
48
|
+
<%= form.label :title, class: label_class, data: { slot: "field-label" } %>
|
|
49
|
+
<%= form.text_field :title,
|
|
50
|
+
class: "cn-input w-full min-w-0 outline-none",
|
|
51
|
+
placeholder: "e.g. Let me export my data as a CSV file",
|
|
52
|
+
maxlength: 120, minlength: 8, autofocus: true,
|
|
53
|
+
"aria-required": true,
|
|
54
|
+
"aria-invalid": (true if title_errors.any?),
|
|
55
|
+
"aria-describedby": ["feature_request_title_hint", ("feature_request_title_error" if title_errors.any?)].compact.join(" ") %>
|
|
56
|
+
<p id="feature_request_title_hint" data-slot="field-description" class="<%= hint_class %>">One line, the way you'd say it to a friend.</p>
|
|
57
|
+
<% if title_errors.any? %>
|
|
58
|
+
<p id="feature_request_title_error" data-slot="field-error" class="cn-field-error font-normal"><%= title_errors.to_sentence %></p>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<% description_errors = errors[:description] %>
|
|
63
|
+
<div data-slot="field" data-invalid="<%= description_errors.any? %>" class="<%= field_class %>">
|
|
64
|
+
<%= form.label :description, class: label_class, data: { slot: "field-label" } %>
|
|
65
|
+
<%= form.text_area :description,
|
|
66
|
+
class: "cn-textarea flex field-sizing-content min-h-16 w-full outline-none",
|
|
67
|
+
rows: 6, maxlength: 2000,
|
|
68
|
+
placeholder: "What are you trying to do, and what gets in the way today?",
|
|
69
|
+
"aria-invalid": (true if description_errors.any?),
|
|
70
|
+
"aria-describedby": ["feature_request_description_hint", ("feature_request_description_error" if description_errors.any?)].compact.join(" ") %>
|
|
71
|
+
<p id="feature_request_description_hint" data-slot="field-description" class="<%= hint_class %>">The problem, not the implementation. We'll figure out the how.</p>
|
|
72
|
+
<% if description_errors.any? %>
|
|
73
|
+
<p id="feature_request_description_error" data-slot="field-error" class="cn-field-error font-normal"><%= description_errors.to_sentence %></p>
|
|
74
|
+
<% end %>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="flex items-center gap-3">
|
|
78
|
+
<button type="submit" name="commit" value="Request it" data-slot="button" data-variant="default" data-size="default"
|
|
79
|
+
class="<%= button %> cn-button-variant-default"><span data-slot="label" class="contents">Request it</span></button>
|
|
80
|
+
<a href="<%= feature_requests_path %>" data-slot="button" data-variant="ghost" data-size="default"
|
|
81
|
+
class="<%= button %> cn-button-variant-ghost"><span data-slot="label" class="contents">Back to your requests</span></a>
|
|
82
|
+
</div>
|
|
83
|
+
<% end %>
|
|
84
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<%# One roadmap entry: the public face of a work item. No spec; that is the
|
|
2
|
+
maintainer's working document. %>
|
|
3
|
+
<article id="<%= dom_id(work_item) %>" data-slot="nr-entry" data-state="ready"
|
|
4
|
+
class="cn-nr-entry flex gap-4 py-5 border-b border-border group/entry cn-nr-entry-state-ready">
|
|
5
|
+
<div data-slot="nr-entry-body" class="cn-nr-entry-body order-1 min-w-0 flex-1">
|
|
6
|
+
<div class="cn-nr-entry-title-row flex flex-wrap items-baseline gap-3">
|
|
7
|
+
<h3 data-slot="nr-entry-title" class="cn-nr-entry-title"><%= work_item.title %></h3>
|
|
8
|
+
<span class="cn-nr-entry-status"><%= work_item_status(work_item, public: true) %></span>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div data-slot="nr-entry-meta" class="cn-nr-entry-meta mt-2 flex flex-wrap items-baseline gap-x-2 gap-y-1 text-sm">
|
|
12
|
+
<span class="cn-nr-entry-meta-item"><%= work_kind_label(work_item) %></span>
|
|
13
|
+
<% if work_item.shipped_at.present? %>
|
|
14
|
+
<span class="cn-nr-entry-meta-sep" aria-hidden="true">·</span>
|
|
15
|
+
<span class="cn-nr-entry-meta-item">shipped <%= work_item.shipped_at.strftime("%b %-d, %Y") %></span>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if (demand = work_item_demand(work_item)) %>
|
|
18
|
+
<span class="cn-nr-entry-meta-sep" aria-hidden="true">·</span>
|
|
19
|
+
<span class="cn-nr-entry-meta-item"><%= demand %></span>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<p data-slot="nr-entry-summary" class="cn-nr-entry-summary mt-2"><%= work_item.summary %></p>
|
|
24
|
+
|
|
25
|
+
<% if work_item.progress_note.present? %>
|
|
26
|
+
<div data-slot="nr-entry-details" class="cn-nr-entry-details mt-4 pt-4">
|
|
27
|
+
<div class="cn-entry-section">
|
|
28
|
+
<h4 class="cn-entry-section-title">Progress</h4>
|
|
29
|
+
<p class="cn-entry-section-content"><%= simple_format(work_item.progress_note, {}, wrapper_tag: "span") %></p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
</article>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<% product = FeatureMachine.config.product_name %>
|
|
2
|
+
<% button = "cn-button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 group/button select-none cn-button-size-default" %>
|
|
3
|
+
<% new_bug_report = FeatureMachine.config.host_path(:new_bug_report_path, self) %>
|
|
4
|
+
<% bug_reports = FeatureMachine.config.host_path(:bug_reports_path, self) %>
|
|
5
|
+
<% content_for :title, "Roadmap — #{product}" %>
|
|
6
|
+
<% content_for :head do %>
|
|
7
|
+
<meta name="robots" content="noindex">
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<div class="cn-container">
|
|
11
|
+
<%= render "feature_machine/flash" %>
|
|
12
|
+
<header class="cn-page-header">
|
|
13
|
+
<p class="cn-page-header-eyebrow">Roadmap</p>
|
|
14
|
+
<h1 class="cn-page-header-title">What's being built, and what shipped</h1>
|
|
15
|
+
<p class="cn-page-header-lede">
|
|
16
|
+
Each entry is a piece of work shaped from what people asked for. Planned
|
|
17
|
+
work first, then the changelog.
|
|
18
|
+
</p>
|
|
19
|
+
<div class="mt-5 flex flex-wrap items-center gap-3">
|
|
20
|
+
<a href="<%= new_feature_request_path %>" data-slot="button" data-variant="default" data-size="default"
|
|
21
|
+
class="<%= button %> cn-button-variant-default"><span data-slot="label" class="contents">Request a feature</span></a>
|
|
22
|
+
<% if new_bug_report %>
|
|
23
|
+
<a href="<%= new_bug_report %>" data-slot="button" data-variant="outline" data-size="default"
|
|
24
|
+
class="<%= button %> cn-button-variant-outline"><span data-slot="label" class="contents">Report a bug</span></a>
|
|
25
|
+
<% end %>
|
|
26
|
+
<a href="<%= feature_requests_path %>" data-slot="button" data-variant="ghost" data-size="default"
|
|
27
|
+
class="<%= button %> cn-button-variant-ghost"><span data-slot="label" class="contents">Your requests</span></a>
|
|
28
|
+
<% if bug_reports %>
|
|
29
|
+
<a href="<%= bug_reports %>" data-slot="button" data-variant="ghost" data-size="default"
|
|
30
|
+
class="<%= button %> cn-button-variant-ghost"><span data-slot="label" class="contents">Your bug reports</span></a>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<% if @roadmap.empty? %>
|
|
36
|
+
<div data-slot="empty" class="cn-empty flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance">
|
|
37
|
+
<div data-slot="empty-header" class="cn-empty-header flex max-w-sm flex-col items-center">
|
|
38
|
+
<h2 data-slot="empty-title" class="cn-empty-title cn-font-heading">Nothing on the roadmap yet</h2>
|
|
39
|
+
<p data-slot="empty-description" class="cn-empty-description text-muted-foreground">Requests are still being triaged. Planned work will appear here.</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<% end %>
|
|
43
|
+
|
|
44
|
+
<% @roadmap.upcoming.each do |status, work_items| %>
|
|
45
|
+
<% next if work_items.empty? %>
|
|
46
|
+
<section class="mt-12" aria-labelledby="roadmap-<%= status %>">
|
|
47
|
+
<h2 id="roadmap-<%= status %>" class="flex items-baseline gap-3">
|
|
48
|
+
<%= work_item_status(work_items.first, public: true) %>
|
|
49
|
+
<span class="text-sm text-muted-foreground"><%= pluralize(work_items.size, "item") %></span>
|
|
50
|
+
</h2>
|
|
51
|
+
<p class="cn-page-header-lede mt-2"><%= work_item_status_blurb(status) %></p>
|
|
52
|
+
<div class="cn-entry-list">
|
|
53
|
+
<%= render partial: "feature_machine/roadmap/work_item", collection: work_items %>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
<% end %>
|
|
57
|
+
|
|
58
|
+
<% if @roadmap.changelog.any? %>
|
|
59
|
+
<section class="mt-12" aria-labelledby="changelog">
|
|
60
|
+
<h2 id="changelog" class="cn-page-header-title">Changelog</h2>
|
|
61
|
+
<p class="cn-page-header-lede mt-2">Features shipped and bugs fixed, newest first.</p>
|
|
62
|
+
<% @roadmap.changelog.each do |month, work_items| %>
|
|
63
|
+
<h3 class="changelog-month" id="changelog-<%= month.strftime("%Y-%m") %>"><%= month.strftime("%B %Y") %></h3>
|
|
64
|
+
<div class="cn-entry-list">
|
|
65
|
+
<%= render partial: "feature_machine/roadmap/work_item", collection: work_items %>
|
|
66
|
+
</div>
|
|
67
|
+
<% end %>
|
|
68
|
+
</section>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%# The engine's standalone shell, used only when a host sets
|
|
2
|
+
FeatureMachine.config.layout = "feature_machine". No host assets are
|
|
3
|
+
assumed: hosts add their own via content_for :head or name their own layout. %>
|
|
4
|
+
<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8">
|
|
8
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
9
|
+
<title><%= content_for?(:title) ? content_for(:title) : FeatureMachine.config.product_name %></title>
|
|
10
|
+
<%= csrf_meta_tags %>
|
|
11
|
+
<%= csp_meta_tag %>
|
|
12
|
+
<%= yield :head %>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<% if notice.present? %><p role="status" data-slot="flash-notice"><%= notice %></p><% end %>
|
|
16
|
+
<% if alert.present? %><p role="alert" data-slot="flash-alert"><%= alert %></p><% end %>
|
|
17
|
+
<main>
|
|
18
|
+
<%= yield %>
|
|
19
|
+
</main>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreateFeatureRequests < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :feature_requests do |t|
|
|
4
|
+
t.references :user, null: false, foreign_key: true
|
|
5
|
+
t.string :title, null: false
|
|
6
|
+
t.text :description
|
|
7
|
+
# requested 0, planned 1, in_progress 2, shipped 3, declined 4 (FeatureRequest#status)
|
|
8
|
+
t.integer :status, null: false, default: 0
|
|
9
|
+
# Public progress note the maintainer writes when the status moves.
|
|
10
|
+
t.text :progress_note
|
|
11
|
+
t.datetime :shipped_at
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :feature_requests, [ :status, :created_at ]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Splits "what a user asked for" (feature_requests, now also bug reports) from
|
|
2
|
+
# "what we will build" (work_items). Requests are bucketed into work items by
|
|
3
|
+
# an admin during triage; work items relate to one another as a graph.
|
|
4
|
+
class CreateWorkItems < ActiveRecord::Migration[8.1]
|
|
5
|
+
def change
|
|
6
|
+
# The embedding columns below need pgvector; a fresh host may not have it yet.
|
|
7
|
+
enable_extension "vector"
|
|
8
|
+
|
|
9
|
+
create_table :work_items do |t|
|
|
10
|
+
t.string :title, null: false
|
|
11
|
+
# feature 0, bug 1 (WorkItem#kind)
|
|
12
|
+
t.integer :kind, null: false, default: 0
|
|
13
|
+
# draft 0, planned 1, in_progress 2, shipped 3, declined 4 (WorkItem#status)
|
|
14
|
+
t.integer :status, null: false, default: 0
|
|
15
|
+
# The hand-off spec.
|
|
16
|
+
t.text :problem
|
|
17
|
+
t.text :scope
|
|
18
|
+
t.text :acceptance_criteria
|
|
19
|
+
# Public: the one-paragraph roadmap/changelog entry and the progress note.
|
|
20
|
+
t.text :summary
|
|
21
|
+
t.text :progress_note
|
|
22
|
+
t.datetime :shipped_at
|
|
23
|
+
t.vector :embedding, limit: 1536
|
|
24
|
+
t.datetime :embedded_at
|
|
25
|
+
t.string :embedding_input_digest
|
|
26
|
+
|
|
27
|
+
t.timestamps
|
|
28
|
+
end
|
|
29
|
+
add_index :work_items, [ :status, :shipped_at ]
|
|
30
|
+
|
|
31
|
+
create_table :work_item_relations do |t|
|
|
32
|
+
t.references :from_work_item, null: false, foreign_key: { to_table: :work_items }
|
|
33
|
+
t.references :to_work_item, null: false, foreign_key: { to_table: :work_items }
|
|
34
|
+
t.string :kind, null: false, default: "related"
|
|
35
|
+
|
|
36
|
+
t.timestamps
|
|
37
|
+
end
|
|
38
|
+
add_index :work_item_relations, [ :from_work_item_id, :to_work_item_id, :kind ],
|
|
39
|
+
unique: true, name: "index_work_item_relations_uniqueness"
|
|
40
|
+
|
|
41
|
+
change_table :feature_requests do |t|
|
|
42
|
+
# feature 0, bug 1 (FeatureRequest#kind)
|
|
43
|
+
t.integer :kind, null: false, default: 0
|
|
44
|
+
t.references :work_item, foreign_key: true
|
|
45
|
+
t.datetime :embedded_at
|
|
46
|
+
t.string :embedding_input_digest
|
|
47
|
+
end
|
|
48
|
+
add_column :feature_requests, :embedding, :vector, limit: 1536
|
|
49
|
+
|
|
50
|
+
add_column :users, :admin, :boolean, null: false, default: false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# The pull request that implements a work item, attached by the agent that
|
|
2
|
+
# opened it when it hands the work back for review.
|
|
3
|
+
class AddPullRequestUrlToWorkItems < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
add_column :work_items, :pull_request_url, :string
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module FeatureMachine
|
|
2
|
+
module Auth
|
|
3
|
+
# Does this credential allow `scope`, by the engine's rules? The credential
|
|
4
|
+
# answers for itself, and for `maintainer` the host's admin predicate must
|
|
5
|
+
# agree. This is the one place the rule lives; controllers and tools call
|
|
6
|
+
# it rather than `credential.allows?` directly.
|
|
7
|
+
def self.allows?(credential, scope)
|
|
8
|
+
return false unless credential&.allows?(scope.to_s)
|
|
9
|
+
return true unless scope.to_s == MAINTAINER
|
|
10
|
+
|
|
11
|
+
FeatureMachine.config.admin?(credential.user)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The "Authorization: Bearer <token>" value of a Rack env, or nil.
|
|
15
|
+
def self.bearer_token(env)
|
|
16
|
+
header = env["HTTP_AUTHORIZATION"].to_s
|
|
17
|
+
scheme, token = header.split(" ", 2)
|
|
18
|
+
token&.strip.presence if scheme&.casecmp?("bearer")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The credential a signed-in browser session carries. `user` is resolved
|
|
22
|
+
# on first use, so installing the middleware costs nothing on requests
|
|
23
|
+
# that never ask. A session user may read and write their own requests;
|
|
24
|
+
# maintainer access needs the host's admin predicate.
|
|
25
|
+
class SessionCredential
|
|
26
|
+
def initialize(request, config: FeatureMachine.config)
|
|
27
|
+
@request = request
|
|
28
|
+
@config = config
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def user
|
|
32
|
+
return @user if defined?(@user)
|
|
33
|
+
|
|
34
|
+
@user = @config.user_for(@request)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def allows?(scope)
|
|
38
|
+
return false unless user
|
|
39
|
+
|
|
40
|
+
case scope.to_s
|
|
41
|
+
when READ, WRITE then true
|
|
42
|
+
when MAINTAINER then @config.admin?(user)
|
|
43
|
+
else false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def session?
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Resolves a bearer token through `config.authenticate_api` and sets the
|
|
53
|
+
# env key when it finds a credential. A missing or bad token sets nothing,
|
|
54
|
+
# so the next middleware may still find one. Install before Session.
|
|
55
|
+
class Bearer
|
|
56
|
+
def initialize(app, resolver: nil)
|
|
57
|
+
@app = app
|
|
58
|
+
@resolver = resolver
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def call(env)
|
|
62
|
+
if env[ENV_KEY].nil? && (resolver = @resolver || FeatureMachine.config.authenticate_api)
|
|
63
|
+
token = Auth.bearer_token(env)
|
|
64
|
+
credential = resolver.call(token) if token
|
|
65
|
+
env[ENV_KEY] = credential if credential
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@app.call(env)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Sets a SessionCredential when nothing earlier in the stack set a
|
|
73
|
+
# credential and `config.session_auth` is on. Install last, after the
|
|
74
|
+
# host's session middleware.
|
|
75
|
+
class Session
|
|
76
|
+
def initialize(app)
|
|
77
|
+
@app = app
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def call(env)
|
|
81
|
+
if env[ENV_KEY].nil? && FeatureMachine.config.session_auth
|
|
82
|
+
env[ENV_KEY] = SessionCredential.new(ActionDispatch::Request.new(env))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@app.call(env)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Mutation calibration cases for FeatureMachine::Judge.
|
|
2
|
+
#
|
|
3
|
+
# Each case is one base request plus two mutants:
|
|
4
|
+
# equivalent: the same request in different words. The answer must not move.
|
|
5
|
+
# faulty: one fact flipped. The answer must move to `faulty.expect`.
|
|
6
|
+
#
|
|
7
|
+
# A judge that moves on paraphrase is reading surface wording; a judge that
|
|
8
|
+
# holds still on a flipped fact isn't reading at all. Requests are drawn from
|
|
9
|
+
# the acceptance personas (lib/acceptance_personas.rb) and `open_items` mirrors
|
|
10
|
+
# the active work items the maintainer persona seeds, so `duplicate_of_open_item`
|
|
11
|
+
# has the same context it gets in triage.
|
|
12
|
+
#
|
|
13
|
+
# Expectations: probability questions use "high" (>= 0.5) or "low" (< 0.5);
|
|
14
|
+
# score questions use one of the judge's labels. `thresholds` bound how far an
|
|
15
|
+
# equivalent mutant may drift from the base before it counts as moved.
|
|
16
|
+
#
|
|
17
|
+
# Run against a live model with `rake feature_machine:calibrate_judge` (opt-in,
|
|
18
|
+
# never in CI). judge_cases_test.rb only checks this file's shape.
|
|
19
|
+
|
|
20
|
+
thresholds:
|
|
21
|
+
probability_drift: 0.3
|
|
22
|
+
score_drift: 0
|
|
23
|
+
|
|
24
|
+
open_items:
|
|
25
|
+
- "[feature] Pin articles to the top of a public list"
|
|
26
|
+
- "[feature] Pending connection suggestions over MCP"
|
|
27
|
+
- "[feature] Webhooks for digest events"
|
|
28
|
+
- "[feature] Mark an article to reread later"
|
|
29
|
+
|
|
30
|
+
cases:
|
|
31
|
+
- id: bug-stuck-drafts
|
|
32
|
+
question: bug
|
|
33
|
+
persona: drafter
|
|
34
|
+
base:
|
|
35
|
+
kind: bug
|
|
36
|
+
title: My drafts never finish fetching details
|
|
37
|
+
description: Three links have said 'Fetching details' since yesterday.
|
|
38
|
+
expect: high
|
|
39
|
+
equivalent:
|
|
40
|
+
kind: bug
|
|
41
|
+
title: Saved links stuck on 'Fetching details'
|
|
42
|
+
description: Since yesterday, three of my saved links are still showing 'Fetching details' and never fill in.
|
|
43
|
+
faulty:
|
|
44
|
+
kind: bug
|
|
45
|
+
title: Show progress while drafts fetch details
|
|
46
|
+
description: Fetching details takes a few seconds on my links. A progress bar would tell me it's working.
|
|
47
|
+
expect: low
|
|
48
|
+
|
|
49
|
+
- id: bug-csv-export
|
|
50
|
+
question: bug
|
|
51
|
+
persona: newcomer
|
|
52
|
+
base:
|
|
53
|
+
kind: feature
|
|
54
|
+
title: Export my library as CSV
|
|
55
|
+
description: I'd like to download everything I've saved as a CSV so I can keep a copy in a spreadsheet.
|
|
56
|
+
expect: low
|
|
57
|
+
equivalent:
|
|
58
|
+
kind: feature
|
|
59
|
+
title: Download my saved articles as a spreadsheet
|
|
60
|
+
description: Could there be a CSV download of my whole library? I want a backup I can open in a spreadsheet.
|
|
61
|
+
faulty:
|
|
62
|
+
kind: feature
|
|
63
|
+
title: Export my library as CSV
|
|
64
|
+
description: I downloaded everything I've saved as a CSV, but the file is empty apart from the header row.
|
|
65
|
+
expect: high
|
|
66
|
+
|
|
67
|
+
- id: duplicate-pinning
|
|
68
|
+
question: duplicate_of_open_item
|
|
69
|
+
persona: curator
|
|
70
|
+
base:
|
|
71
|
+
kind: feature
|
|
72
|
+
title: Keep my favourite articles at the top of my public page
|
|
73
|
+
description: I want two or three pieces to stay first on my public list instead of sliding down as I save new ones.
|
|
74
|
+
expect: high
|
|
75
|
+
equivalent:
|
|
76
|
+
kind: feature
|
|
77
|
+
title: Let me pin a few articles on my public list
|
|
78
|
+
description: New saves push my best pieces off the top of my public page. I'd like to fix a handful in place above everything else.
|
|
79
|
+
faulty:
|
|
80
|
+
kind: feature
|
|
81
|
+
title: Hide some articles from my public page
|
|
82
|
+
description: I want two or three pieces to stay off my public list while still being in my library.
|
|
83
|
+
expect: low
|
|
84
|
+
|
|
85
|
+
- id: duplicate-mcp-tagging
|
|
86
|
+
question: duplicate_of_open_item
|
|
87
|
+
persona: agentic
|
|
88
|
+
base:
|
|
89
|
+
kind: feature
|
|
90
|
+
title: Let agents tag articles over MCP
|
|
91
|
+
description: My agent can save and reflect on articles but has no way to add or remove tags on them.
|
|
92
|
+
expect: low
|
|
93
|
+
equivalent:
|
|
94
|
+
kind: feature
|
|
95
|
+
title: MCP tool for article tags
|
|
96
|
+
description: Agents can add articles and reflections through MCP, but there's no tool to set an article's tags.
|
|
97
|
+
faulty:
|
|
98
|
+
kind: feature
|
|
99
|
+
title: Let agents see suggested connections over MCP
|
|
100
|
+
description: My agent can create connections but has no way to list the ones the app has already suggested.
|
|
101
|
+
expect: high
|
|
102
|
+
|
|
103
|
+
- id: clarity-reading-time
|
|
104
|
+
question: clarity
|
|
105
|
+
persona: reader
|
|
106
|
+
base:
|
|
107
|
+
kind: feature
|
|
108
|
+
title: Show reading time on my public list
|
|
109
|
+
description: My newsletter subscribers click through from my public list and ask how long each piece is. A minutes estimate next to each article would let them pick what fits their time.
|
|
110
|
+
expect: High
|
|
111
|
+
equivalent:
|
|
112
|
+
kind: feature
|
|
113
|
+
title: Reading-time estimates on public list articles
|
|
114
|
+
description: People who subscribe to my newsletter browse my public list and want to know how long an article takes before opening it. Please show an estimated read time beside each one.
|
|
115
|
+
faulty:
|
|
116
|
+
kind: feature
|
|
117
|
+
title: Make my public list better
|
|
118
|
+
description: My newsletter subscribers use my public list. It could be better for them.
|
|
119
|
+
expect: Low
|
|
120
|
+
|
|
121
|
+
- id: clarity-no-description
|
|
122
|
+
question: clarity
|
|
123
|
+
persona: Edge_Case-99
|
|
124
|
+
base:
|
|
125
|
+
kind: feature
|
|
126
|
+
title: No description
|
|
127
|
+
description: ""
|
|
128
|
+
expect: Low
|
|
129
|
+
equivalent:
|
|
130
|
+
kind: feature
|
|
131
|
+
title: An idea
|
|
132
|
+
description: ""
|
|
133
|
+
faulty:
|
|
134
|
+
kind: feature
|
|
135
|
+
title: Search my reflections, not just article titles
|
|
136
|
+
description: I write a lot of reflections and can't find them later. The library search only matches titles, so I want it to match the text of my reflections too.
|
|
137
|
+
expect: High
|