Almirah 0.4.4 → 0.4.5
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 +4 -4
- data/lib/almirah/doc_fabric.rb +0 -6
- data/lib/almirah/doc_items/scope_table.rb +39 -88
- data/lib/almirah/doc_types/base_document.rb +1 -11
- data/lib/almirah/doc_types/decision.rb +1 -126
- data/lib/almirah/doc_types/decisions_overview.rb +5 -555
- data/lib/almirah/doc_types/planning_dates.rb +2 -3
- data/lib/almirah/project/project_data.rb +1 -4
- data/lib/almirah/project.rb +0 -122
- data/lib/almirah/project_configuration.rb +0 -99
- data/lib/almirah/templates/css/main.css +0 -157
- metadata +1 -8
- data/lib/almirah/doc_items/work_item.rb +0 -129
- data/lib/almirah/doc_types/critical_chain_page.rb +0 -218
- data/lib/almirah/doc_types/decision_grouping.rb +0 -17
- data/lib/almirah/project/critical_chain.rb +0 -117
- data/lib/almirah/project/fever_chart.rb +0 -94
- data/lib/almirah/project/work_item_scheduler.rb +0 -167
- data/lib/almirah/project/working_calendar.rb +0 -112
data/lib/almirah/project.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require_relative 'doc_fabric'
|
|
5
|
-
require_relative 'doc_items/work_item'
|
|
6
5
|
require_relative 'navigation_pane'
|
|
7
6
|
require_relative 'doc_types/traceability'
|
|
8
7
|
require_relative 'doc_types/index'
|
|
@@ -53,7 +52,6 @@ class Project
|
|
|
53
52
|
link_all_risks
|
|
54
53
|
check_wrong_specification_referenced
|
|
55
54
|
build_link_registry
|
|
56
|
-
link_work_items
|
|
57
55
|
create_index
|
|
58
56
|
render_all_specifications(@project_data.specifications)
|
|
59
57
|
render_all_specifications(@project_data.traceability_matrices)
|
|
@@ -62,7 +60,6 @@ class Project
|
|
|
62
60
|
render_all_source_files
|
|
63
61
|
render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
|
|
64
62
|
render_decisions_overview
|
|
65
|
-
render_critical_chain_page
|
|
66
63
|
render_all_decisions
|
|
67
64
|
render_all_risk_records
|
|
68
65
|
render_risk_registry_pages
|
|
@@ -70,7 +67,6 @@ class Project
|
|
|
70
67
|
render_index
|
|
71
68
|
create_search_data
|
|
72
69
|
report_broken_links
|
|
73
|
-
report_kit_violations
|
|
74
70
|
report_rendered
|
|
75
71
|
end
|
|
76
72
|
|
|
@@ -87,7 +83,6 @@ class Project
|
|
|
87
83
|
link_all_risks
|
|
88
84
|
check_wrong_specification_referenced
|
|
89
85
|
build_link_registry
|
|
90
|
-
link_work_items
|
|
91
86
|
create_index
|
|
92
87
|
render_all_specifications(@project_data.specifications)
|
|
93
88
|
render_all_specifications(@project_data.traceability_matrices)
|
|
@@ -96,7 +91,6 @@ class Project
|
|
|
96
91
|
render_all_source_files
|
|
97
92
|
render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
|
|
98
93
|
render_decisions_overview
|
|
99
|
-
render_critical_chain_page
|
|
100
94
|
render_all_decisions
|
|
101
95
|
render_all_risk_records
|
|
102
96
|
render_risk_registry_pages
|
|
@@ -104,7 +98,6 @@ class Project
|
|
|
104
98
|
render_index
|
|
105
99
|
create_search_data
|
|
106
100
|
report_broken_links
|
|
107
|
-
report_kit_violations
|
|
108
101
|
report_rendered
|
|
109
102
|
end
|
|
110
103
|
|
|
@@ -124,111 +117,6 @@ class Project
|
|
|
124
117
|
broken.each { |b| puts ConsoleReporter.warn_detail(" #{b[:document] || '?'}: #{b[:target]}") }
|
|
125
118
|
end
|
|
126
119
|
|
|
127
|
-
# Builds the per-row WorkItem dependency network (ADR-194): registers every
|
|
128
|
-
# Scope-row work item, fills the intra-record step-order edges, then resolves
|
|
129
|
-
# each Depends On reference globally (LinkRegistry) to the activity-type-aligned
|
|
130
|
-
# work item of the target record, tagging each cross-record edge in-group or
|
|
131
|
-
# cross-group against the decision_groups boundary (ADR-197). Unresolved
|
|
132
|
-
# references are collected for report_kit_violations. Runs after
|
|
133
|
-
# build_link_registry (so every record resolves) and before rendering (so the
|
|
134
|
-
# overview Kit column is ready).
|
|
135
|
-
def link_work_items
|
|
136
|
-
@kit_unresolved = []
|
|
137
|
-
@project_data.decisions.each do |d|
|
|
138
|
-
d.scope_work_items.each { |wi| @project_data.work_items[wi.id] = wi }
|
|
139
|
-
end
|
|
140
|
-
link_intra_record_steps
|
|
141
|
-
link_cross_record_dependencies
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Each row's lower-numbered same-record steps are its predecessors; equal step
|
|
145
|
-
# numbers are concurrent (no edge). These edges are in-group by definition.
|
|
146
|
-
def link_intra_record_steps
|
|
147
|
-
@project_data.decisions.each do |d|
|
|
148
|
-
items = d.scope_work_items
|
|
149
|
-
items.each do |wi|
|
|
150
|
-
items.each do |other|
|
|
151
|
-
next if other.equal?(wi) || other.step >= wi.step
|
|
152
|
-
|
|
153
|
-
wi.add_predecessor(other, cross_group: false)
|
|
154
|
-
other.add_successor(wi)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def link_cross_record_dependencies
|
|
161
|
-
@project_data.decisions.each do |d|
|
|
162
|
-
d.scope_work_items.each do |wi|
|
|
163
|
-
wi.depends_on_refs.each { |ref| link_dependency(d, wi, ref) }
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def link_dependency(record, work_item, ref)
|
|
169
|
-
target = @project_data.link_registry.find_by_id(ref)
|
|
170
|
-
unless target.is_a?(Decision)
|
|
171
|
-
@kit_unresolved << { record: record.id, target: ref }
|
|
172
|
-
return
|
|
173
|
-
end
|
|
174
|
-
prereq = aligned_work_item(target, work_item.activity)
|
|
175
|
-
return if prereq.nil? || prereq.equal?(work_item)
|
|
176
|
-
|
|
177
|
-
cross = decision_group_name(record) != decision_group_name(target)
|
|
178
|
-
work_item.add_predecessor(prereq, cross_group: cross)
|
|
179
|
-
prereq.add_successor(work_item)
|
|
180
|
-
anchor = target.scope_table&.step_column? ? prereq.row_anchor : nil
|
|
181
|
-
work_item.add_resolved_dependency(ref, target, anchor, prereq.id)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# The target record's work item whose activity (Item) matches `activity`,
|
|
185
|
-
# falling back to the nearest earlier activity by canonical phase order, then
|
|
186
|
-
# (when the target has only later activities) to its earliest row. nil only
|
|
187
|
-
# when the target has no Scope rows.
|
|
188
|
-
def aligned_work_item(target, activity)
|
|
189
|
-
items = target.scope_work_items
|
|
190
|
-
return nil if items.empty?
|
|
191
|
-
|
|
192
|
-
exact = items.select { |t| t.activity == activity }.min_by(&:step)
|
|
193
|
-
return exact if exact
|
|
194
|
-
|
|
195
|
-
rank = WorkItem::ACTIVITY_ORDER.index(activity) || WorkItem::ACTIVITY_ORDER.length
|
|
196
|
-
earlier = items.select { |t| t.activity_rank <= rank }
|
|
197
|
-
return items.min_by { |t| [t.activity_rank, t.step] } if earlier.empty?
|
|
198
|
-
|
|
199
|
-
earlier.min_by { |t| [-t.activity_rank, t.step] }
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# The planning-group name (first-level decisions/ folder) a record belongs to,
|
|
203
|
-
# read from the decision_groups collection (ADR-197).
|
|
204
|
-
def decision_group_name(doc)
|
|
205
|
-
group = @project_data.decision_groups.find { |g| g.values.first.include?(doc) }
|
|
206
|
-
group&.keys&.first
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Reports the two kit gates and unresolved Depends On references (ADR-194), all
|
|
210
|
-
# as non-failing console warnings alongside report_broken_links.
|
|
211
|
-
def report_kit_violations
|
|
212
|
-
@kit_unresolved ||= []
|
|
213
|
-
phase = @project_data.work_items.each_value.select(&:phase_order_violation?)
|
|
214
|
-
cross = @project_data.work_items.each_value.select(&:cross_record_violation?)
|
|
215
|
-
total = phase.length + cross.length + @kit_unresolved.length
|
|
216
|
-
return if total.zero?
|
|
217
|
-
|
|
218
|
-
ConsoleReporter.warn('kit violations', total)
|
|
219
|
-
phase.each do |wi|
|
|
220
|
-
blocking = wi.intra_record_predecessors.reject(&:done?).map(&:id).join(', ')
|
|
221
|
-
puts ConsoleReporter.warn_detail(" phase order: #{wi.id} started before #{blocking}")
|
|
222
|
-
end
|
|
223
|
-
cross.each do |wi|
|
|
224
|
-
blocking = wi.cross_record_predecessors.reject(&:done?).map(&:id).join(', ')
|
|
225
|
-
puts ConsoleReporter.warn_detail(" not kitted: #{wi.id} needs #{blocking}")
|
|
226
|
-
end
|
|
227
|
-
@kit_unresolved.each do |u|
|
|
228
|
-
puts ConsoleReporter.warn_detail(" unresolved Depends On: #{u[:record]} -> #{u[:target]}")
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
|
|
232
120
|
# Assigns each document its generated output path (relative to the build root)
|
|
233
121
|
# and registers it for cross-document link resolution (ADR-186). Runs after all
|
|
234
122
|
# documents are parsed and before any rendering, so link targets are known.
|
|
@@ -611,16 +499,6 @@ class Project
|
|
|
611
499
|
doc.to_html("#{path}/build/decisions/")
|
|
612
500
|
end
|
|
613
501
|
|
|
614
|
-
def render_critical_chain_page
|
|
615
|
-
return if @project_data.decisions.empty?
|
|
616
|
-
|
|
617
|
-
path = @configuration.project_root_directory
|
|
618
|
-
FileUtils.mkdir_p("#{path}/build/decisions")
|
|
619
|
-
|
|
620
|
-
doc = DocFabric.create_critical_chain_page(@project)
|
|
621
|
-
doc.to_html("#{path}/build/decisions/")
|
|
622
|
-
end
|
|
623
|
-
|
|
624
502
|
def render_all_decisions
|
|
625
503
|
return if @project_data.decisions.empty?
|
|
626
504
|
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
require 'yaml'
|
|
2
|
-
require 'date'
|
|
3
2
|
|
|
4
3
|
class ProjectConfiguration
|
|
5
|
-
DEFAULT_WIP_LIMIT = 2
|
|
6
|
-
DEFAULT_BUFFER_RATIO = 0.5
|
|
7
|
-
DEFAULT_HOURS_PER_DAY = 8
|
|
8
|
-
|
|
9
4
|
attr_accessor :project_root_directory, :parameters
|
|
10
5
|
|
|
11
6
|
def initialize(path)
|
|
@@ -34,78 +29,6 @@ class ProjectConfiguration
|
|
|
34
29
|
[]
|
|
35
30
|
end
|
|
36
31
|
|
|
37
|
-
def get_wip_limit
|
|
38
|
-
return DEFAULT_WIP_LIMIT unless @parameters.is_a?(Hash)
|
|
39
|
-
|
|
40
|
-
planning = @parameters['planning']
|
|
41
|
-
return DEFAULT_WIP_LIMIT unless planning.is_a?(Hash)
|
|
42
|
-
|
|
43
|
-
value = planning['wip_limit']
|
|
44
|
-
return DEFAULT_WIP_LIMIT unless value.is_a?(Integer) && value.positive?
|
|
45
|
-
|
|
46
|
-
value
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# The CCPM project-buffer ratio (ADR-195): a fraction in (0, 1] cutting the
|
|
50
|
-
# aggregated chain safety. Absent or out-of-range falls back to the 0.5 default.
|
|
51
|
-
def get_buffer_ratio
|
|
52
|
-
return DEFAULT_BUFFER_RATIO unless @parameters.is_a?(Hash)
|
|
53
|
-
|
|
54
|
-
planning = @parameters['planning']
|
|
55
|
-
return DEFAULT_BUFFER_RATIO unless planning.is_a?(Hash)
|
|
56
|
-
|
|
57
|
-
value = planning['buffer_ratio']
|
|
58
|
-
return DEFAULT_BUFFER_RATIO unless value.is_a?(Numeric) && value.positive? && value <= 1
|
|
59
|
-
|
|
60
|
-
value
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# The calendar anchor for working day 1 (ADR-205), a Date parsed from a
|
|
64
|
-
# DD-MM-YYYY planning.start_date. Absent or unparseable falls back to today,
|
|
65
|
-
# so an unconfigured project still renders (a moving anchor).
|
|
66
|
-
def get_start_date
|
|
67
|
-
date = parse_planning_date(planning_value('start_date'))
|
|
68
|
-
date || Date.today
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Per-group planning start dates (ADR-211): a map from a decision group's
|
|
72
|
-
# first-level folder name (under decisions/) to its start Date, read from
|
|
73
|
-
# planning.groups as DD-MM-YYYY entries. Non-string or unparseable values are
|
|
74
|
-
# dropped; empty when unset. A group absent from the map sequences after the
|
|
75
|
-
# previous group rather than carrying a declared start.
|
|
76
|
-
def get_group_start_dates
|
|
77
|
-
value = planning_value('groups')
|
|
78
|
-
return {} unless value.is_a?(Hash)
|
|
79
|
-
|
|
80
|
-
value.each_with_object({}) do |(name, raw), acc|
|
|
81
|
-
date = parse_planning_date(raw)
|
|
82
|
-
acc[name.to_s] = date if date
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# The non-working holiday dates (ADR-205) from planning.holidays, each a
|
|
87
|
-
# DD-MM-YYYY entry; unparseable entries are dropped. Empty when unset.
|
|
88
|
-
def get_holidays
|
|
89
|
-
value = planning_value('holidays')
|
|
90
|
-
return [] unless value.is_a?(Array)
|
|
91
|
-
|
|
92
|
-
value.filter_map { |entry| parse_planning_date(entry) }
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Working hours per day (ADR-196): converts logged effort hours into the
|
|
96
|
-
# working-day unit the estimates use. Absent or non-positive falls back to 8.
|
|
97
|
-
def get_hours_per_day
|
|
98
|
-
return DEFAULT_HOURS_PER_DAY unless @parameters.is_a?(Hash)
|
|
99
|
-
|
|
100
|
-
planning = @parameters['planning']
|
|
101
|
-
return DEFAULT_HOURS_PER_DAY unless planning.is_a?(Hash)
|
|
102
|
-
|
|
103
|
-
value = planning['hours_per_day']
|
|
104
|
-
return DEFAULT_HOURS_PER_DAY unless value.is_a?(Numeric) && value.positive?
|
|
105
|
-
|
|
106
|
-
value
|
|
107
|
-
end
|
|
108
|
-
|
|
109
32
|
# The ordered register-column list for a risk registry folder (ADR-216),
|
|
110
33
|
# read from the risks: root — a list of { folder:, columns: } entries.
|
|
111
34
|
# nil when the registry carries no configuration; such a registry renders
|
|
@@ -164,26 +87,4 @@ class ProjectConfiguration
|
|
|
164
87
|
def numeric_threshold(value)
|
|
165
88
|
value.is_a?(Numeric) ? value : nil
|
|
166
89
|
end
|
|
167
|
-
|
|
168
|
-
# A value under the planning: key, or nil when planning is absent.
|
|
169
|
-
def planning_value(key)
|
|
170
|
-
return nil unless @parameters.is_a?(Hash)
|
|
171
|
-
|
|
172
|
-
planning = @parameters['planning']
|
|
173
|
-
planning.is_a?(Hash) ? planning[key] : nil
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Parse a planning date that may already be a Date (YAML ISO form) or a
|
|
177
|
-
# DD-MM-YYYY string; nil when neither.
|
|
178
|
-
def parse_planning_date(value)
|
|
179
|
-
return value if value.is_a?(Date)
|
|
180
|
-
return nil unless value.is_a?(String)
|
|
181
|
-
|
|
182
|
-
match = /\A(\d{2})-(\d{2})-(\d{4})\z/.match(value.strip)
|
|
183
|
-
return nil unless match
|
|
184
|
-
|
|
185
|
-
Date.new(match[3].to_i, match[2].to_i, match[1].to_i)
|
|
186
|
-
rescue ArgumentError
|
|
187
|
-
nil
|
|
188
|
-
end
|
|
189
90
|
end
|
|
@@ -147,163 +147,6 @@ table.controlled td.item_text{
|
|
|
147
147
|
max-width: 100%;
|
|
148
148
|
max-height: 300px;
|
|
149
149
|
}
|
|
150
|
-
.workitem_gantt {
|
|
151
|
-
--gantt-owner-width: 120px;
|
|
152
|
-
--gantt-day-width: 26px;
|
|
153
|
-
--gantt-month-h: 18px;
|
|
154
|
-
overflow: auto;
|
|
155
|
-
max-height: 420px;
|
|
156
|
-
margin-bottom: 16px;
|
|
157
|
-
border: 1px solid #e1e4e8;
|
|
158
|
-
}
|
|
159
|
-
.workitem_gantt .gantt_grid {
|
|
160
|
-
display: grid;
|
|
161
|
-
gap: 0;
|
|
162
|
-
width: max-content;
|
|
163
|
-
padding: 4px;
|
|
164
|
-
}
|
|
165
|
-
.workitem_gantt .gantt_corner,
|
|
166
|
-
.workitem_gantt .gantt_owner,
|
|
167
|
-
.workitem_gantt .gantt_owner_tracking,
|
|
168
|
-
.workitem_gantt .gantt_band_corner,
|
|
169
|
-
.workitem_gantt .gantt_buffer {
|
|
170
|
-
position: sticky;
|
|
171
|
-
left: 0;
|
|
172
|
-
background: #fff;
|
|
173
|
-
padding: 2px 8px;
|
|
174
|
-
font-weight: 600;
|
|
175
|
-
white-space: nowrap;
|
|
176
|
-
border-right: 1px solid #e1e4e8;
|
|
177
|
-
z-index: 2;
|
|
178
|
-
}
|
|
179
|
-
.workitem_gantt .gantt_corner {
|
|
180
|
-
top: 0;
|
|
181
|
-
border-bottom: 1px solid #bbb;
|
|
182
|
-
z-index: 3;
|
|
183
|
-
}
|
|
184
|
-
.workitem_gantt .gantt_month_head {
|
|
185
|
-
position: sticky;
|
|
186
|
-
top: 0;
|
|
187
|
-
background: #fff;
|
|
188
|
-
text-align: center;
|
|
189
|
-
font-size: 11px;
|
|
190
|
-
font-weight: 600;
|
|
191
|
-
color: #57606a;
|
|
192
|
-
border-bottom: 1px solid #e1e4e8;
|
|
193
|
-
border-left: 1px solid #d0d7de;
|
|
194
|
-
white-space: nowrap;
|
|
195
|
-
overflow: hidden;
|
|
196
|
-
z-index: 1;
|
|
197
|
-
}
|
|
198
|
-
.workitem_gantt .gantt_day_head {
|
|
199
|
-
position: sticky;
|
|
200
|
-
top: var(--gantt-month-h);
|
|
201
|
-
background: #fff;
|
|
202
|
-
text-align: center;
|
|
203
|
-
font-size: 11px;
|
|
204
|
-
color: #57606a;
|
|
205
|
-
border-bottom: 1px solid #bbb;
|
|
206
|
-
z-index: 1;
|
|
207
|
-
}
|
|
208
|
-
/* Weekday-holiday highlight in the Chart.js palette red (ADR-205 amendment):
|
|
209
|
-
the column carries the buffer bar's diagonal stripe texture, in red; the
|
|
210
|
-
header stays a solid tint so the day number reads. */
|
|
211
|
-
.workitem_gantt .gantt_day_head.gantt_nonworking { background: rgba(255, 99, 132, 0.22); color: #b23a52; }
|
|
212
|
-
.workitem_gantt .gantt_day_head.gantt_friday { background: rgb(225, 241, 250); }
|
|
213
|
-
.workitem_gantt .gantt_nonworking_col {
|
|
214
|
-
background: repeating-linear-gradient(45deg,
|
|
215
|
-
rgba(255, 99, 132, 0.20), rgba(255, 99, 132, 0.20) 5px,
|
|
216
|
-
rgba(255, 99, 132, 0.07) 5px, rgba(255, 99, 132, 0.07) 10px);
|
|
217
|
-
}
|
|
218
|
-
.workitem_gantt .gantt_friday_col { background: rgb(225, 241, 250); }
|
|
219
|
-
.workitem_gantt .gantt_bar,
|
|
220
|
-
.workitem_gantt .gantt_track_bar {
|
|
221
|
-
font-size: 11px;
|
|
222
|
-
line-height: 1.5;
|
|
223
|
-
margin: 1px;
|
|
224
|
-
padding: 1px 5px;
|
|
225
|
-
border-radius: 3px;
|
|
226
|
-
overflow: hidden;
|
|
227
|
-
white-space: nowrap;
|
|
228
|
-
text-overflow: ellipsis;
|
|
229
|
-
color: #fff;
|
|
230
|
-
cursor: default;
|
|
231
|
-
}
|
|
232
|
-
.workitem_gantt .gantt_todo { background: #9aa5b1; }
|
|
233
|
-
.workitem_gantt .gantt_inprogress { background: #ffffdd; color: #333; box-shadow: inset 0 0 0 1px #bbb; }
|
|
234
|
-
.workitem_gantt .gantt_done { background: #cfc; color: #57606a; box-shadow: inset 0 0 0 1px #e1e4e8; }
|
|
235
|
-
.workitem_gantt .gantt_blocked { box-shadow: inset 0 0 0 1px #bbb; }
|
|
236
|
-
.workitem_gantt .gantt_critical { outline: 1px solid #9a6700; outline-offset: -1px; } /* buffer-bar amber (dark) */
|
|
237
|
-
.workitem_gantt .gantt_release_band {
|
|
238
|
-
text-align: center;
|
|
239
|
-
font-size: 11px;
|
|
240
|
-
font-weight: 600;
|
|
241
|
-
color: #57606a;
|
|
242
|
-
background: #eef1f4;
|
|
243
|
-
border-bottom: 1px solid #e1e4e8;
|
|
244
|
-
border-left: 1px solid #d0d7de;
|
|
245
|
-
padding: 2px 4px;
|
|
246
|
-
white-space: nowrap;
|
|
247
|
-
overflow: hidden;
|
|
248
|
-
text-overflow: ellipsis;
|
|
249
|
-
}
|
|
250
|
-
.workitem_gantt .gantt_buffer { color: #9a6700; }
|
|
251
|
-
.critical_chain { margin-bottom: 16px; }
|
|
252
|
-
.critical_chain h2 { font-size: 1.1em; margin-bottom: 6px; }
|
|
253
|
-
.critical_chain .cc_group { margin: 8px 0; }
|
|
254
|
-
.critical_chain .cc_group h3 { font-size: 0.95em; margin: 4px 0; }
|
|
255
|
-
.critical_chain table.cc_chain { border-collapse: collapse; margin: 4px 0; }
|
|
256
|
-
.critical_chain table.cc_chain th,
|
|
257
|
-
.critical_chain table.cc_chain td { border: 1px solid #e1e4e8; padding: 2px 8px; text-align: left; font-size: 12px; }
|
|
258
|
-
.critical_chain .cc_buffer,
|
|
259
|
-
.critical_chain .cc_consumed,
|
|
260
|
-
.critical_chain .cc_projected,
|
|
261
|
-
.critical_chain .cc_finish { margin: 2px 0; font-size: 12px; }
|
|
262
|
-
.critical_chain .cc_consumed { color: #57606a; }
|
|
263
|
-
.critical_chain .cc_finish { font-weight: 600; }
|
|
264
|
-
.critical_chain .cc_unestimated { color: #9a6700; font-style: italic; font-size: 12px; }
|
|
265
|
-
.critical_chain .cc_group_body { display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start; }
|
|
266
|
-
.critical_chain .cc_plan { flex: 1 1 320px; }
|
|
267
|
-
.critical_chain .cc_fever { flex: 1 1 360px; max-width: 480px; height: 320px; position: relative; }
|
|
268
|
-
.critical_chain .cc_fever .fever_canvas { width: 100%; height: 100%; }
|
|
269
|
-
.workitem_gantt .gantt_buffer_bar {
|
|
270
|
-
font-size: 11px;
|
|
271
|
-
line-height: 1.5;
|
|
272
|
-
margin: 1px;
|
|
273
|
-
padding: 1px 5px;
|
|
274
|
-
border-radius: 3px;
|
|
275
|
-
text-align: center;
|
|
276
|
-
overflow: hidden;
|
|
277
|
-
white-space: nowrap;
|
|
278
|
-
text-overflow: ellipsis;
|
|
279
|
-
color: #9a6700;
|
|
280
|
-
background: repeating-linear-gradient(45deg, #ffe9b3, #ffe9b3 5px, #fff3d4 5px, #fff3d4 10px);
|
|
281
|
-
box-shadow: inset 0 0 0 1px #e0c068;
|
|
282
|
-
cursor: default;
|
|
283
|
-
}
|
|
284
|
-
.workitem_gantt .gantt_today { border-left: 2px solid #d33; z-index: 5; pointer-events: none; }
|
|
285
|
-
/* Plan-vs-actual tracking lanes (ADR-213): hidden until the toolbar toggle adds
|
|
286
|
-
show-actuals; a hidden lane's grid tracks collapse to zero height. */
|
|
287
|
-
.workitem_gantt .gantt_tracking_lane { display: none; }
|
|
288
|
-
.workitem_gantt.show-actuals .gantt_tracking_lane { display: block; }
|
|
289
|
-
.workitem_gantt .gantt_owner_tracking { font-weight: 400; font-style: italic; color: #57606a; }
|
|
290
|
-
.workitem_gantt .gantt_track_committed { background: #b0b6bd; }
|
|
291
|
-
.workitem_gantt .gantt_track_logged { background: #36a2eb; }
|
|
292
|
-
.gantt_container .gantt_toolbar {
|
|
293
|
-
display: flex;
|
|
294
|
-
justify-content: flex-end;
|
|
295
|
-
gap: 6px;
|
|
296
|
-
padding: 4px 0;
|
|
297
|
-
}
|
|
298
|
-
.gantt_container .gantt_actuals_toggle {
|
|
299
|
-
font-size: 12px;
|
|
300
|
-
padding: 3px 10px;
|
|
301
|
-
border: 1px solid #d0d7de;
|
|
302
|
-
border-radius: 4px;
|
|
303
|
-
background: #f6f8fa;
|
|
304
|
-
cursor: pointer;
|
|
305
|
-
}
|
|
306
|
-
.gantt_container .gantt_actuals_toggle:hover { background: #eef1f4; }
|
|
307
150
|
table.controlled:not(.odd-even) tbody tr:nth-child(odd) { background-color:#f6f7f8; }
|
|
308
151
|
table.controlled:not(.odd-even) tbody tr:nth-child(even) { background-color: #fff; }
|
|
309
152
|
table.controlled:not(.odd-even) tbody tr:nth-child(odd):hover,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: Almirah
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oleksandr Ivanov
|
|
@@ -77,13 +77,10 @@ files:
|
|
|
77
77
|
- lib/almirah/doc_items/source_code_paragraph.rb
|
|
78
78
|
- lib/almirah/doc_items/text_line.rb
|
|
79
79
|
- lib/almirah/doc_items/todo_block.rb
|
|
80
|
-
- lib/almirah/doc_items/work_item.rb
|
|
81
80
|
- lib/almirah/doc_parser.rb
|
|
82
81
|
- lib/almirah/doc_types/base_document.rb
|
|
83
82
|
- lib/almirah/doc_types/coverage.rb
|
|
84
|
-
- lib/almirah/doc_types/critical_chain_page.rb
|
|
85
83
|
- lib/almirah/doc_types/decision.rb
|
|
86
|
-
- lib/almirah/doc_types/decision_grouping.rb
|
|
87
84
|
- lib/almirah/doc_types/decisions_overview.rb
|
|
88
85
|
- lib/almirah/doc_types/implementation.rb
|
|
89
86
|
- lib/almirah/doc_types/index.rb
|
|
@@ -103,12 +100,8 @@ files:
|
|
|
103
100
|
- lib/almirah/link_registry.rb
|
|
104
101
|
- lib/almirah/navigation_pane.rb
|
|
105
102
|
- lib/almirah/project.rb
|
|
106
|
-
- lib/almirah/project/critical_chain.rb
|
|
107
103
|
- lib/almirah/project/doc_linker.rb
|
|
108
|
-
- lib/almirah/project/fever_chart.rb
|
|
109
104
|
- lib/almirah/project/project_data.rb
|
|
110
|
-
- lib/almirah/project/work_item_scheduler.rb
|
|
111
|
-
- lib/almirah/project/working_calendar.rb
|
|
112
105
|
- lib/almirah/project_configuration.rb
|
|
113
106
|
- lib/almirah/project_template.rb
|
|
114
107
|
- lib/almirah/project_utility.rb
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# A single Scope-table row of a Decision Record, modelled as a node in the
|
|
4
|
-
# per-row dependency network (ADR-194). Each work item carries its canonical
|
|
5
|
-
# identity (`<record>.<step>.<activity>`), the bounded per-row Status it reads
|
|
6
|
-
# its readiness from (ADR-193), and predecessor / successor edges spanning both
|
|
7
|
-
# intra-record (step order) and cross-record (Depends On) links.
|
|
8
|
-
#
|
|
9
|
-
# `predecessors` / `successors` follow the house list-of-single-key-hashes shape
|
|
10
|
-
# (ADR-197): each entry maps a readable `record.step.activity` label to the
|
|
11
|
-
# resolved WorkItem. Every cross-record edge additionally records whether it
|
|
12
|
-
# stays within a decision_groups folder (in-group) or crosses to another
|
|
13
|
-
# (cross-group); that tag is inert here and consumed by the critical-chain step
|
|
14
|
-
# (ADR-195). Readiness ignores it — a cross-group predecessor blocks exactly
|
|
15
|
-
# like an in-group one.
|
|
16
|
-
class WorkItem
|
|
17
|
-
# Canonical phase order used only as the fallback for activity-type-aligned
|
|
18
|
-
# resolution when the prerequisite has no row of the dependent's exact Item.
|
|
19
|
-
ACTIVITY_ORDER = %w[Analysis Requirements Code Tests].freeze
|
|
20
|
-
|
|
21
|
-
attr_reader :record_id, :step, :activity, :owner, :status, :depends_on_refs,
|
|
22
|
-
:focused_estimate, :safe_estimate, :record_sequence, :start_date, :target_date
|
|
23
|
-
attr_accessor :predecessors, :successors, :cross_group_predecessor_labels, :resolved_dependencies
|
|
24
|
-
|
|
25
|
-
def initialize(record_id:, step:, activity:, owner:, status:, depends_on_refs:, # rubocop:disable Metrics/ParameterLists
|
|
26
|
-
focused_estimate: 0.0, safe_estimate: 0.0, record_sequence: 0,
|
|
27
|
-
start_date: nil, target_date: nil)
|
|
28
|
-
@record_id = record_id
|
|
29
|
-
@step = step
|
|
30
|
-
@activity = activity.to_s.strip
|
|
31
|
-
@owner = owner.to_s.strip
|
|
32
|
-
@status = status.to_s.strip
|
|
33
|
-
@depends_on_refs = depends_on_refs # array of record ref strings, e.g. ["ADR-193"]
|
|
34
|
-
# The row's authored committed window (ADR-213): the Scope Start Date and
|
|
35
|
-
# Target Date as parsed Dates (nil when absent/unparseable). Distinct from the
|
|
36
|
-
# computed schedule -- these are the author's intent, drawn on the tracking lane.
|
|
37
|
-
@start_date = start_date
|
|
38
|
-
@target_date = target_date
|
|
39
|
-
# Per-row scheduling estimates in working days (ADR-195); the focused estimate
|
|
40
|
-
# is the scheduling duration, safe - focused the safety aggregated into the
|
|
41
|
-
# project buffer. record_sequence is the owning record's number, used only as
|
|
42
|
-
# a deterministic priority tiebreak in the critical-chain scheduler.
|
|
43
|
-
@focused_estimate = focused_estimate
|
|
44
|
-
@safe_estimate = safe_estimate
|
|
45
|
-
@record_sequence = record_sequence
|
|
46
|
-
@predecessors = []
|
|
47
|
-
@successors = []
|
|
48
|
-
@cross_group_predecessor_labels = []
|
|
49
|
-
# One entry per resolved Depends On reference, keyed by the authored ref:
|
|
50
|
-
# { ref => { doc:, anchor:, label: } }. `anchor` is the target's resolved
|
|
51
|
-
# work-item row anchor (nil when the target has no `#` step column, so the
|
|
52
|
-
# link opens the record page); `label` is the resolved work item's id, for
|
|
53
|
-
# the link tooltip. Used to render the Depends On cell as deep links.
|
|
54
|
-
@resolved_dependencies = {}
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def id
|
|
58
|
-
"#{@record_id}.#{@step}.#{@activity}"
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# The anchor of this work item's row in its rendered Scope table — the deep-link
|
|
62
|
-
# target a dependent record points at. Namespaced with `.scope.` so it never
|
|
63
|
-
# collides with the Affected Documents controlled table, whose rows anchor on
|
|
64
|
-
# the same `<record>.<step>` scheme on the same page.
|
|
65
|
-
def row_anchor
|
|
66
|
-
"#{@record_id}.scope.#{@step}"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def add_resolved_dependency(ref, doc, anchor, label)
|
|
70
|
-
@resolved_dependencies[ref] = { doc: doc, anchor: anchor, label: label }
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def activity_rank
|
|
74
|
-
ACTIVITY_ORDER.index(@activity) || ACTIVITY_ORDER.length
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# A row is "started" once it leaves To Do; both In-Progress and Done count, so
|
|
78
|
-
# a Done row whose predecessor never finished is still flagged as a violation.
|
|
79
|
-
def started?
|
|
80
|
-
@status == 'In-Progress' || @status == 'Done'
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def done?
|
|
84
|
-
@status == 'Done'
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def add_predecessor(work_item, cross_group:)
|
|
88
|
-
@predecessors << { work_item.id => work_item }
|
|
89
|
-
@cross_group_predecessor_labels << work_item.id if cross_group
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def add_successor(work_item)
|
|
93
|
-
@successors << { work_item.id => work_item }
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def predecessor_items
|
|
97
|
-
@predecessors.map { |edge| edge.values.first }
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def successor_items
|
|
101
|
-
@successors.map { |edge| edge.values.first }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Same-record, lower-numbered steps — the intra-record (phase-order) edges.
|
|
105
|
-
def intra_record_predecessors
|
|
106
|
-
predecessor_items.select { |p| p.record_id == @record_id }
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Resolved Depends On edges into other records (in-group or cross-group alike).
|
|
110
|
-
def cross_record_predecessors
|
|
111
|
-
predecessor_items.reject { |p| p.record_id == @record_id }
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Kitted when every predecessor — intra- and cross-record, regardless of the
|
|
115
|
-
# in-group / cross-group tag — is Done; trivially kitted when it has none.
|
|
116
|
-
def fully_kitted?
|
|
117
|
-
predecessor_items.all?(&:done?)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# A started row with an unfinished lower-numbered step in the same record.
|
|
121
|
-
def phase_order_violation?
|
|
122
|
-
started? && intra_record_predecessors.any? { |p| !p.done? }
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# A started row whose resolved cross-record predecessor is not yet Done.
|
|
126
|
-
def cross_record_violation?
|
|
127
|
-
started? && cross_record_predecessors.any? { |p| !p.done? }
|
|
128
|
-
end
|
|
129
|
-
end
|