retold-facto 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.dockerignore +8 -0
  3. package/.quackage.json +19 -0
  4. package/Dockerfile +26 -0
  5. package/bin/retold-facto.js +909 -0
  6. package/examples/facto-government-data.sqlite +0 -0
  7. package/examples/government-data-catalog.json +137 -0
  8. package/examples/government-data-loader.js +1432 -0
  9. package/package.json +91 -0
  10. package/scripts/facto-download.js +425 -0
  11. package/source/Retold-Facto.js +1042 -0
  12. package/source/services/Retold-Facto-BeaconProvider.js +511 -0
  13. package/source/services/Retold-Facto-CatalogManager.js +1252 -0
  14. package/source/services/Retold-Facto-DataLakeService.js +1642 -0
  15. package/source/services/Retold-Facto-DatasetManager.js +417 -0
  16. package/source/services/Retold-Facto-IngestEngine.js +1315 -0
  17. package/source/services/Retold-Facto-ProjectionEngine.js +3960 -0
  18. package/source/services/Retold-Facto-RecordManager.js +360 -0
  19. package/source/services/Retold-Facto-SchemaManager.js +1110 -0
  20. package/source/services/Retold-Facto-SourceFolderScanner.js +2243 -0
  21. package/source/services/Retold-Facto-SourceManager.js +730 -0
  22. package/source/services/Retold-Facto-StoreConnectionManager.js +441 -0
  23. package/source/services/Retold-Facto-ThroughputMonitor.js +478 -0
  24. package/source/services/web-app/codemirror-entry.js +7 -0
  25. package/source/services/web-app/pict-app/Pict-Application-Facto-Configuration.json +9 -0
  26. package/source/services/web-app/pict-app/Pict-Application-Facto.js +70 -0
  27. package/source/services/web-app/pict-app/Pict-Facto-Bundle.js +11 -0
  28. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto-UI.js +66 -0
  29. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto.js +69 -0
  30. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Catalog.js +93 -0
  31. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Connections.js +42 -0
  32. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Datasets.js +605 -0
  33. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Projections.js +188 -0
  34. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Scanner.js +80 -0
  35. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Schema.js +116 -0
  36. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Sources.js +104 -0
  37. package/source/services/web-app/pict-app/views/PictView-Facto-Catalog.js +526 -0
  38. package/source/services/web-app/pict-app/views/PictView-Facto-Datasets.js +173 -0
  39. package/source/services/web-app/pict-app/views/PictView-Facto-Ingest.js +259 -0
  40. package/source/services/web-app/pict-app/views/PictView-Facto-Layout.js +191 -0
  41. package/source/services/web-app/pict-app/views/PictView-Facto-Projections.js +231 -0
  42. package/source/services/web-app/pict-app/views/PictView-Facto-Records.js +326 -0
  43. package/source/services/web-app/pict-app/views/PictView-Facto-Scanner.js +624 -0
  44. package/source/services/web-app/pict-app/views/PictView-Facto-Sources.js +201 -0
  45. package/source/services/web-app/pict-app/views/PictView-Facto-Throughput.js +456 -0
  46. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full-Configuration.json +14 -0
  47. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full.js +391 -0
  48. package/source/services/web-app/pict-app-full/providers/PictRouter-Facto-Configuration.json +56 -0
  49. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-BottomBar.js +68 -0
  50. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Connections.js +340 -0
  51. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboard.js +149 -0
  52. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboards.js +819 -0
  53. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Datasets.js +178 -0
  54. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-IngestJobs.js +99 -0
  55. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Layout.js +62 -0
  56. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-MappingEditor.js +158 -0
  57. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-ProjectionDetail.js +1120 -0
  58. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Projections.js +172 -0
  59. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-QueryPanel.js +119 -0
  60. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-RecordViewer.js +663 -0
  61. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Records.js +648 -0
  62. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Scanner.js +1017 -0
  63. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDetail.js +1404 -0
  64. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDocEditor.js +1036 -0
  65. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaEditor.js +636 -0
  66. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaResearch.js +357 -0
  67. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceDetail.js +822 -0
  68. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceEditor.js +1036 -0
  69. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceResearch.js +487 -0
  70. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Sources.js +165 -0
  71. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Throughput.js +439 -0
  72. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-TopBar.js +335 -0
  73. package/source/services/web-app/pict-app-full/views/projections/Facto-Projections-Constants.js +71 -0
  74. package/source/services/web-app/web/chart.min.js +20 -0
  75. package/source/services/web-app/web/codemirror-bundle.js +30099 -0
  76. package/source/services/web-app/web/css/facto-themes.css +467 -0
  77. package/source/services/web-app/web/css/facto.css +502 -0
  78. package/source/services/web-app/web/index.html +28 -0
  79. package/source/services/web-app/web/retold-facto.js +12138 -0
  80. package/source/services/web-app/web/retold-facto.js.map +1 -0
  81. package/source/services/web-app/web/retold-facto.min.js +2 -0
  82. package/source/services/web-app/web/retold-facto.min.js.map +1 -0
  83. package/source/services/web-app/web/simple/index.html +17 -0
  84. package/test/Facto_Browser_Integration_tests.js +798 -0
  85. package/test/RetoldFacto_tests.js +4117 -0
  86. package/test/fixtures/weather-readings.csv +17 -0
  87. package/test/fixtures/weather-stations.csv +9 -0
  88. package/test/model/MeadowModel-Extended.json +8497 -0
  89. package/test/model/MeadowModel-PICT.json +1 -0
  90. package/test/model/MeadowModel.json +1355 -0
  91. package/test/model/ddl/Facto.ddl +225 -0
  92. package/test/model/fable-configuration.json +14 -0
@@ -0,0 +1,231 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ class FactoProjectionsView extends libPictView
4
+ {
5
+ constructor(pFable, pOptions, pServiceHash)
6
+ {
7
+ super(pFable, pOptions, pServiceHash);
8
+ }
9
+
10
+ onAfterRender()
11
+ {
12
+ this.loadSummary();
13
+ }
14
+
15
+ loadSummary()
16
+ {
17
+ this.pict.providers.Facto.loadProjectionSummary().then(
18
+ (pResponse) =>
19
+ {
20
+ let tmpContainer = document.getElementById('facto-projections-summary');
21
+ if (!tmpContainer || !pResponse) return;
22
+
23
+ let tmpHtml = '<table><tbody>';
24
+ tmpHtml += '<tr><td style="font-weight:600;">Sources</td><td>' + (pResponse.Sources || 0) + '</td>';
25
+ tmpHtml += '<td style="font-weight:600;">Datasets</td><td>' + (pResponse.Datasets || 0) + '</td></tr>';
26
+ tmpHtml += '<tr><td style="font-weight:600;">Records</td><td>' + (pResponse.Records || 0) + '</td>';
27
+ tmpHtml += '<td style="font-weight:600;">Certainty Indices</td><td>' + (pResponse.CertaintyIndices || 0) + '</td></tr>';
28
+ tmpHtml += '<tr><td style="font-weight:600;">Ingest Jobs</td><td>' + (pResponse.IngestJobs || 0) + '</td>';
29
+ tmpHtml += '<td></td><td></td></tr>';
30
+
31
+ if (pResponse.DatasetsByType)
32
+ {
33
+ tmpHtml += '<tr><td colspan="4" style="padding-top:12px; font-weight:600; border-bottom:2px solid #ddd;">Datasets by Type</td></tr>';
34
+ tmpHtml += '<tr>';
35
+ tmpHtml += '<td><span class="badge badge-raw">Raw</span></td><td>' + (pResponse.DatasetsByType.Raw || 0) + '</td>';
36
+ tmpHtml += '<td><span class="badge badge-compositional">Compositional</span></td><td>' + (pResponse.DatasetsByType.Compositional || 0) + '</td>';
37
+ tmpHtml += '</tr><tr>';
38
+ tmpHtml += '<td><span class="badge badge-projection">Projection</span></td><td>' + (pResponse.DatasetsByType.Projection || 0) + '</td>';
39
+ tmpHtml += '<td><span class="badge badge-derived">Derived</span></td><td>' + (pResponse.DatasetsByType.Derived || 0) + '</td>';
40
+ tmpHtml += '</tr>';
41
+ }
42
+
43
+ tmpHtml += '</tbody></table>';
44
+ tmpContainer.innerHTML = tmpHtml;
45
+ }).catch(
46
+ (pError) =>
47
+ {
48
+ this.pict.views['Pict-Section-Modal'].toast('Error loading summary: ' + pError.message, {type: 'error'});
49
+ });
50
+ }
51
+
52
+ runQuery()
53
+ {
54
+ let tmpDatasetIDsRaw = this.pict.providers.FactoUI.getVal('facto-proj-dataset-ids');
55
+ let tmpType = this.pict.providers.FactoUI.getVal('facto-proj-type');
56
+ let tmpCertaintyThreshold = parseFloat(this.pict.providers.FactoUI.getVal('facto-proj-certainty')) || 0;
57
+ let tmpTimeStart = this.pict.providers.FactoUI.getVal('facto-proj-time-start');
58
+ let tmpTimeStop = this.pict.providers.FactoUI.getVal('facto-proj-time-stop');
59
+
60
+ let tmpDatasetIDs = tmpDatasetIDsRaw.split(',').map(function(s) { return parseInt(s.trim(), 10); }).filter(function(n) { return !isNaN(n) && n > 0; });
61
+ if (tmpDatasetIDs.length === 0)
62
+ {
63
+ this.pict.views['Pict-Section-Modal'].toast('Enter at least one Dataset ID', {type: 'warning'});
64
+ return;
65
+ }
66
+
67
+ let tmpParams = { DatasetIDs: tmpDatasetIDs, Begin: 0, Cap: 100 };
68
+ if (tmpType) tmpParams.Type = tmpType;
69
+ if (tmpCertaintyThreshold > 0) tmpParams.CertaintyThreshold = tmpCertaintyThreshold;
70
+ if (tmpTimeStart) tmpParams.TimeRangeStart = parseInt(tmpTimeStart, 10) || 0;
71
+ if (tmpTimeStop) tmpParams.TimeRangeStop = parseInt(tmpTimeStop, 10) || 0;
72
+
73
+ this.pict.views['Pict-Section-Modal'].toast('Querying...', {type: 'info'});
74
+
75
+ this.pict.providers.Facto.queryRecords(tmpParams).then(
76
+ (pResponse) =>
77
+ {
78
+ this.refreshResults(pResponse);
79
+ }).catch(
80
+ (pError) =>
81
+ {
82
+ this.pict.views['Pict-Section-Modal'].toast('Query error: ' + pError.message, {type: 'error'});
83
+ });
84
+ }
85
+
86
+ runCompare()
87
+ {
88
+ let tmpDatasetIDsRaw = this.pict.providers.FactoUI.getVal('facto-proj-dataset-ids');
89
+ let tmpDatasetIDs = tmpDatasetIDsRaw.split(',').map(function(s) { return parseInt(s.trim(), 10); }).filter(function(n) { return !isNaN(n) && n > 0; });
90
+
91
+ if (tmpDatasetIDs.length === 0)
92
+ {
93
+ this.pict.views['Pict-Section-Modal'].toast('Enter at least one Dataset ID', {type: 'warning'});
94
+ return;
95
+ }
96
+
97
+ this.pict.views['Pict-Section-Modal'].toast('Comparing...', {type: 'info'});
98
+
99
+ this.pict.providers.Facto.compareDatasets(tmpDatasetIDs).then(
100
+ (pResponse) =>
101
+ {
102
+ let tmpContainer = document.getElementById('facto-projections-results');
103
+ if (!tmpContainer || !pResponse || !pResponse.Datasets) return;
104
+
105
+ let tmpHtml = '<h4 style="margin:12px 0 8px; font-size:0.95em;">Dataset Comparison</h4>';
106
+ tmpHtml += '<table><thead><tr><th>ID</th><th>Name</th><th>Type</th><th>Records</th><th>Sources</th></tr></thead><tbody>';
107
+ for (let i = 0; i < pResponse.Datasets.length; i++)
108
+ {
109
+ let tmpDS = pResponse.Datasets[i];
110
+ let tmpTypeLower = (tmpDS.Type || '').toLowerCase();
111
+ tmpHtml += '<tr>';
112
+ tmpHtml += '<td>' + (tmpDS.IDDataset || '') + '</td>';
113
+ tmpHtml += '<td>' + (tmpDS.Name || '') + '</td>';
114
+ tmpHtml += '<td><span class="badge badge-' + tmpTypeLower + '">' + (tmpDS.Type || '') + '</span></td>';
115
+ tmpHtml += '<td>' + (tmpDS.RecordCount || 0) + '</td>';
116
+ tmpHtml += '<td>' + (tmpDS.SourceCount || 0) + '</td>';
117
+ tmpHtml += '</tr>';
118
+ }
119
+ tmpHtml += '</tbody></table>';
120
+ tmpContainer.innerHTML = tmpHtml;
121
+ }).catch(
122
+ (pError) =>
123
+ {
124
+ this.pict.views['Pict-Section-Modal'].toast('Compare error: ' + pError.message, {type: 'error'});
125
+ });
126
+ }
127
+
128
+ refreshResults(pResponse)
129
+ {
130
+ let tmpContainer = document.getElementById('facto-projections-results');
131
+ if (!tmpContainer) return;
132
+
133
+ if (!pResponse || !pResponse.Records || pResponse.Records.length === 0)
134
+ {
135
+ tmpContainer.innerHTML = '<p style="color:#888; font-style:italic;">No records match the query.</p>';
136
+ return;
137
+ }
138
+
139
+ let tmpHtml = '<h4 style="margin:12px 0 8px; font-size:0.95em;">Query Results (' + (pResponse.Count || pResponse.Records.length) + ' records)</h4>';
140
+ tmpHtml += '<table><thead><tr><th>ID</th><th>Dataset</th><th>Source</th><th>Type</th><th>Version</th><th>Ingest Date</th><th>Content</th></tr></thead><tbody>';
141
+ for (let i = 0; i < pResponse.Records.length; i++)
142
+ {
143
+ let tmpRecord = pResponse.Records[i];
144
+ let tmpContent = tmpRecord.Content || '';
145
+ if (tmpContent.length > 80) tmpContent = tmpContent.substring(0, 80) + '...';
146
+ tmpHtml += '<tr>';
147
+ tmpHtml += '<td>' + (tmpRecord.IDRecord || '') + '</td>';
148
+ tmpHtml += '<td>' + (tmpRecord.IDDataset || '') + '</td>';
149
+ tmpHtml += '<td>' + (tmpRecord.IDSource || '') + '</td>';
150
+ tmpHtml += '<td>' + (tmpRecord.Type || '') + '</td>';
151
+ tmpHtml += '<td>' + (tmpRecord.Version || 1) + '</td>';
152
+ tmpHtml += '<td>' + (tmpRecord.IngestDate || '-') + '</td>';
153
+ tmpHtml += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">' + tmpContent + '</td>';
154
+ tmpHtml += '</tr>';
155
+ }
156
+ tmpHtml += '</tbody></table>';
157
+ tmpContainer.innerHTML = tmpHtml;
158
+ }
159
+ }
160
+
161
+ module.exports = FactoProjectionsView;
162
+
163
+ module.exports.default_configuration =
164
+ {
165
+ ViewIdentifier: 'Facto-Projections',
166
+ DefaultRenderable: 'Facto-Projections',
167
+ DefaultDestinationAddress: '#Facto-Section-Projections',
168
+ Templates:
169
+ [
170
+ {
171
+ Hash: 'Facto-Projections',
172
+ Template: /*html*/`
173
+ <div class="accordion-row">
174
+ <div class="accordion-number">5</div>
175
+ <div class="accordion-card" id="facto-card-projections">
176
+ <div class="accordion-header" onclick="pict.views['Facto-Layout'].toggleSection('facto-card-projections')">
177
+ <span class="accordion-title">Projections</span>
178
+ <span class="accordion-preview">Cross-dataset queries and warehouse statistics</span>
179
+ <span class="accordion-toggle">&#9660;</span>
180
+ </div>
181
+ <div class="accordion-body">
182
+ <p style="margin-bottom:12px; color:#666; font-size:0.9em;">Query across datasets, compare data, and view warehouse-wide statistics.</p>
183
+
184
+ <h3 style="margin-bottom:8px; font-size:1em; color:#444;">Warehouse Summary</h3>
185
+ <div id="facto-projections-summary" style="margin-bottom:16px;"><p style="color:#888; font-style:italic;">Loading...</p></div>
186
+
187
+ <h3 style="margin-top:16px; margin-bottom:8px; font-size:1em; color:#444;">Cross-Dataset Query</h3>
188
+ <div class="inline-group">
189
+ <div>
190
+ <label for="facto-proj-dataset-ids">Dataset IDs (comma-separated)</label>
191
+ <input type="text" id="facto-proj-dataset-ids" placeholder="1,2,3">
192
+ </div>
193
+ <div>
194
+ <label for="facto-proj-type">Record Type (optional)</label>
195
+ <input type="text" id="facto-proj-type" placeholder="e.g. enrollment">
196
+ </div>
197
+ </div>
198
+ <div class="inline-group">
199
+ <div>
200
+ <label for="facto-proj-certainty">Min Certainty (0-1)</label>
201
+ <input type="text" id="facto-proj-certainty" placeholder="0.0">
202
+ </div>
203
+ <div>
204
+ <label for="facto-proj-time-start">Time Range Start</label>
205
+ <input type="text" id="facto-proj-time-start" placeholder="timestamp">
206
+ </div>
207
+ <div>
208
+ <label for="facto-proj-time-stop">Time Range Stop</label>
209
+ <input type="text" id="facto-proj-time-stop" placeholder="timestamp">
210
+ </div>
211
+ </div>
212
+ <button class="primary" onclick="pict.views['Facto-Projections'].runQuery()">Query</button>
213
+ <button class="secondary" onclick="pict.views['Facto-Projections'].runCompare()">Compare Datasets</button>
214
+ <button class="secondary" onclick="pict.views['Facto-Projections'].loadSummary()">Refresh Summary</button>
215
+
216
+ <div id="facto-projections-results" style="margin-top:16px;"></div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ `
221
+ }
222
+ ],
223
+ Renderables:
224
+ [
225
+ {
226
+ RenderableHash: 'Facto-Projections',
227
+ TemplateHash: 'Facto-Projections',
228
+ DestinationAddress: '#Facto-Section-Projections'
229
+ }
230
+ ]
231
+ };
@@ -0,0 +1,326 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ class FactoRecordsView extends libPictView
4
+ {
5
+ constructor(pFable, pOptions, pServiceHash)
6
+ {
7
+ super(pFable, pOptions, pServiceHash);
8
+ }
9
+
10
+ onAfterRender()
11
+ {
12
+ this.pict.providers.Facto.loadRecords(0).then(
13
+ () =>
14
+ {
15
+ this.refreshList();
16
+ }).catch(
17
+ (pError) =>
18
+ {
19
+ this.pict.views['Pict-Section-Modal'].toast('Error loading records: ' + pError.message, {type: 'error'});
20
+ });
21
+ }
22
+
23
+ refreshList()
24
+ {
25
+ let tmpContainer = document.getElementById('facto-records-list');
26
+ if (!tmpContainer) return;
27
+
28
+ let tmpRecords = this.pict.AppData.Facto.Records;
29
+ if (!tmpRecords || tmpRecords.length === 0)
30
+ {
31
+ tmpContainer.innerHTML = '<p style="color:#888; font-style:italic;">No records ingested yet.</p>';
32
+ return;
33
+ }
34
+
35
+ let tmpHtml = '<table><thead><tr><th>ID</th><th>Type</th><th>Dataset</th><th>Source</th><th>Version</th><th>Ingest Date</th><th>Content Preview</th><th>Actions</th></tr></thead><tbody>';
36
+ for (let i = 0; i < tmpRecords.length; i++)
37
+ {
38
+ let tmpRecord = tmpRecords[i];
39
+ let tmpPreview = (tmpRecord.Content || '').substring(0, 60);
40
+ if ((tmpRecord.Content || '').length > 60) tmpPreview += '...';
41
+ tmpHtml += '<tr>';
42
+ tmpHtml += '<td>' + (tmpRecord.IDRecord || '') + '</td>';
43
+ tmpHtml += '<td>' + (tmpRecord.Type || '') + '</td>';
44
+ tmpHtml += '<td>' + (tmpRecord.IDDataset || '') + '</td>';
45
+ tmpHtml += '<td>' + (tmpRecord.IDSource || '') + '</td>';
46
+ tmpHtml += '<td>' + (tmpRecord.Version || '1') + '</td>';
47
+ tmpHtml += '<td>' + (tmpRecord.IngestDate || '') + '</td>';
48
+ tmpHtml += '<td style="max-width:250px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:monospace; font-size:0.85em;">' + tmpPreview + '</td>';
49
+ tmpHtml += '<td><button class="secondary" style="padding:4px 8px; font-size:0.8em;" onclick="pict.views[\'Facto-Records\'].viewCertainty(' + tmpRecord.IDRecord + ')">Certainty</button></td>';
50
+ tmpHtml += '</tr>';
51
+ }
52
+ tmpHtml += '</tbody></table>';
53
+
54
+ // Pagination controls
55
+ let tmpPage = this.pict.AppData.Facto.RecordPage || 0;
56
+ tmpHtml += '<div style="margin-top:12px; display:flex; gap:8px; align-items:center;">';
57
+ if (tmpPage > 0)
58
+ {
59
+ tmpHtml += '<button class="secondary" style="padding:4px 12px; font-size:0.85em;" onclick="pict.views[\'Facto-Records\'].changePage(' + (tmpPage - 1) + ')">&#9664; Previous</button>';
60
+ }
61
+ tmpHtml += '<span style="color:#888; font-size:0.85em;">Page ' + (tmpPage + 1) + '</span>';
62
+ if (tmpRecords.length >= this.pict.AppData.Facto.RecordPageSize)
63
+ {
64
+ tmpHtml += '<button class="secondary" style="padding:4px 12px; font-size:0.85em;" onclick="pict.views[\'Facto-Records\'].changePage(' + (tmpPage + 1) + ')">Next &#9654;</button>';
65
+ }
66
+ tmpHtml += '</div>';
67
+
68
+ tmpContainer.innerHTML = tmpHtml;
69
+ }
70
+
71
+ changePage(pPage)
72
+ {
73
+ this.pict.AppData.Facto.RecordPage = pPage;
74
+ this.pict.providers.Facto.loadRecords(pPage).then(
75
+ () =>
76
+ {
77
+ this.refreshList();
78
+ }).catch(
79
+ (pError) =>
80
+ {
81
+ this.pict.views['Pict-Section-Modal'].toast('Error: ' + pError.message, {type: 'error'});
82
+ });
83
+ }
84
+
85
+ viewCertainty(pIDRecord)
86
+ {
87
+ this.pict.providers.Facto.loadRecordCertainty(pIDRecord).then(
88
+ (pResponse) =>
89
+ {
90
+ let tmpPanel = document.getElementById('facto-certainty-panel');
91
+
92
+ if (!pResponse || !pResponse.CertaintyIndices || pResponse.CertaintyIndices.length === 0)
93
+ {
94
+ if (tmpPanel) tmpPanel.innerHTML = this._renderCertaintyEmpty(pIDRecord);
95
+ else this.pict.views['Pict-Section-Modal'].toast('No certainty data for record #' + pIDRecord, {type: 'info'});
96
+ return;
97
+ }
98
+
99
+ let tmpHtml = this._renderCertaintyPanel(pIDRecord, pResponse.CertaintyIndices);
100
+ if (tmpPanel)
101
+ {
102
+ tmpPanel.innerHTML = tmpHtml;
103
+ tmpPanel.style.display = 'block';
104
+ }
105
+ else
106
+ {
107
+ // Insert panel after the records list
108
+ let tmpList = document.getElementById('facto-records-list');
109
+ if (tmpList)
110
+ {
111
+ let tmpDiv = document.createElement('div');
112
+ tmpDiv.id = 'facto-certainty-panel';
113
+ tmpDiv.innerHTML = tmpHtml;
114
+ tmpList.parentNode.insertBefore(tmpDiv, tmpList.nextSibling);
115
+ }
116
+ }
117
+ }).catch(
118
+ (pError) =>
119
+ {
120
+ this.pict.views['Pict-Section-Modal'].toast('Error: ' + pError.message, {type: 'error'});
121
+ });
122
+ }
123
+
124
+ closeCertaintyPanel()
125
+ {
126
+ let tmpPanel = document.getElementById('facto-certainty-panel');
127
+ if (tmpPanel)
128
+ {
129
+ tmpPanel.style.display = 'none';
130
+ }
131
+ }
132
+
133
+ _renderCertaintyEmpty(pIDRecord)
134
+ {
135
+ return /*html*/`
136
+ <div style="border:1px solid var(--facto-border); border-radius:6px; padding:16px; margin-top:16px; background:var(--facto-bg-surface);">
137
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
138
+ <h4 style="margin:0; color:var(--facto-text-heading);">Certainty — Record #${pIDRecord}</h4>
139
+ <button class="secondary" style="padding:2px 10px; font-size:0.8em;" onclick="pict.views['Facto-Records'].closeCertaintyPanel()">Close</button>
140
+ </div>
141
+ <p style="color:var(--facto-text-secondary); font-style:italic; margin:0;">No certainty indices recorded for this record. Run a multi-set projection merge to generate field-level certainty scores.</p>
142
+ </div>`;
143
+ }
144
+
145
+ _renderCertaintyPanel(pIDRecord, pIndices)
146
+ {
147
+ // Group indices by field name
148
+ let tmpFields = {};
149
+ for (let i = 0; i < pIndices.length; i++)
150
+ {
151
+ let tmpCI = pIndices[i];
152
+ let tmpDimension = tmpCI.Dimension || '';
153
+
154
+ // Parse dimension format: "field:{FieldName}:{dimension}"
155
+ let tmpMatch = tmpDimension.match(/^field:(.+):(presence|ratio|composite)$/);
156
+ if (tmpMatch)
157
+ {
158
+ let tmpFieldName = tmpMatch[1];
159
+ let tmpDimType = tmpMatch[2];
160
+ if (!tmpFields[tmpFieldName])
161
+ {
162
+ tmpFields[tmpFieldName] = { presence: 0, ratio: 0, composite: 0, justification: null };
163
+ }
164
+ tmpFields[tmpFieldName][tmpDimType] = tmpCI.CertaintyValue;
165
+ if (tmpCI.Justification && !tmpFields[tmpFieldName].justification)
166
+ {
167
+ try { tmpFields[tmpFieldName].justification = JSON.parse(tmpCI.Justification); }
168
+ catch (e) { /* ignore parse errors */ }
169
+ }
170
+ }
171
+ }
172
+
173
+ let tmpFieldNames = Object.keys(tmpFields);
174
+ if (tmpFieldNames.length === 0)
175
+ {
176
+ // Fall back to raw display for non-field-level indices
177
+ return this._renderCertaintyRaw(pIDRecord, pIndices);
178
+ }
179
+
180
+ // Compute record-level composite
181
+ let tmpCompositeSum = 0;
182
+ for (let i = 0; i < tmpFieldNames.length; i++)
183
+ {
184
+ tmpCompositeSum += tmpFields[tmpFieldNames[i]].composite;
185
+ }
186
+ let tmpRecordComposite = tmpFieldNames.length > 0 ? tmpCompositeSum / tmpFieldNames.length : 0.5;
187
+
188
+ // Build the panel HTML
189
+ let tmpHtml = /*html*/`
190
+ <div style="border:1px solid var(--facto-border); border-radius:6px; padding:16px; margin-top:16px; background:var(--facto-bg-surface);">
191
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
192
+ <h4 style="margin:0; color:var(--facto-text-heading);">Certainty — Record #${pIDRecord}</h4>
193
+ <button class="secondary" style="padding:2px 10px; font-size:0.8em;" onclick="pict.views['Facto-Records'].closeCertaintyPanel()">Close</button>
194
+ </div>
195
+ <div style="display:flex; align-items:center; gap:12px; margin-bottom:16px; padding:10px; background:var(--facto-bg-elevated); border-radius:4px;">
196
+ <span style="font-weight:600; color:var(--facto-text-heading);">Record Composite</span>
197
+ ${this._renderBar(tmpRecordComposite, 200)}
198
+ <span style="font-weight:600; font-size:1.1em; color:${this._certaintyColor(tmpRecordComposite)};">${(tmpRecordComposite * 100).toFixed(1)}%</span>
199
+ </div>
200
+ <table style="width:100%; border-collapse:collapse;">
201
+ <thead>
202
+ <tr style="border-bottom:2px solid var(--facto-border);">
203
+ <th style="text-align:left; padding:6px 8px; color:var(--facto-text-heading);">Field</th>
204
+ <th style="text-align:left; padding:6px 8px; color:var(--facto-text-heading);">Presence</th>
205
+ <th style="text-align:left; padding:6px 8px; color:var(--facto-text-heading);">Ratio</th>
206
+ <th style="text-align:left; padding:6px 8px; color:var(--facto-text-heading);">Composite</th>
207
+ <th style="text-align:center; padding:6px 8px; color:var(--facto-text-heading);">Sources</th>
208
+ </tr>
209
+ </thead>
210
+ <tbody>`;
211
+
212
+ for (let i = 0; i < tmpFieldNames.length; i++)
213
+ {
214
+ let tmpFN = tmpFieldNames[i];
215
+ let tmpF = tmpFields[tmpFN];
216
+ let tmpJ = tmpF.justification || {};
217
+ let tmpSources = (tmpJ.agreeing || 0) + '✓';
218
+ if (tmpJ.conflicting > 0)
219
+ {
220
+ tmpSources += ' ' + tmpJ.conflicting + '✗';
221
+ }
222
+
223
+ tmpHtml += /*html*/`
224
+ <tr style="border-bottom:1px solid var(--facto-border-subtle);">
225
+ <td style="padding:6px 8px; font-family:monospace; font-size:0.9em; color:var(--facto-text);">${tmpFN}</td>
226
+ <td style="padding:6px 8px;">${this._renderBar(tmpF.presence, 100)} <span style="font-size:0.85em; color:var(--facto-text-secondary);">${(tmpF.presence * 100).toFixed(0)}%</span></td>
227
+ <td style="padding:6px 8px;">${this._renderBar(tmpF.ratio, 100)} <span style="font-size:0.85em; color:var(--facto-text-secondary);">${(tmpF.ratio * 100).toFixed(0)}%</span></td>
228
+ <td style="padding:6px 8px;">${this._renderBar(tmpF.composite, 100)} <span style="font-weight:600; font-size:0.85em; color:${this._certaintyColor(tmpF.composite)};">${(tmpF.composite * 100).toFixed(0)}%</span></td>
229
+ <td style="padding:6px 8px; text-align:center; font-size:0.85em; color:var(--facto-text-secondary);">${tmpSources}</td>
230
+ </tr>`;
231
+ }
232
+
233
+ tmpHtml += /*html*/`
234
+ </tbody>
235
+ </table>
236
+ <div style="margin-top:10px; font-size:0.8em; color:var(--facto-text-tertiary);">
237
+ <strong>Presence</strong> = cross-dataset corroboration &nbsp;|&nbsp;
238
+ <strong>Ratio</strong> = within-dataset frequency &nbsp;|&nbsp;
239
+ <strong>Composite</strong> = weighted (${((tmpFields[tmpFieldNames[0]] && tmpFields[tmpFieldNames[0]].justification && tmpFields[tmpFieldNames[0]].justification.weights) ? (tmpFields[tmpFieldNames[0]].justification.weights.presence * 100).toFixed(0) + '/' + (tmpFields[tmpFieldNames[0]].justification.weights.ratio * 100).toFixed(0) : '70/30')}) &nbsp;|&nbsp;
240
+ ✓ = agreeing sources &nbsp; ✗ = conflicting sources
241
+ </div>
242
+ </div>`;
243
+
244
+ return tmpHtml;
245
+ }
246
+
247
+ _renderCertaintyRaw(pIDRecord, pIndices)
248
+ {
249
+ let tmpHtml = /*html*/`
250
+ <div style="border:1px solid var(--facto-border); border-radius:6px; padding:16px; margin-top:16px; background:var(--facto-bg-surface);">
251
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
252
+ <h4 style="margin:0; color:var(--facto-text-heading);">Certainty — Record #${pIDRecord}</h4>
253
+ <button class="secondary" style="padding:2px 10px; font-size:0.8em;" onclick="pict.views['Facto-Records'].closeCertaintyPanel()">Close</button>
254
+ </div>`;
255
+
256
+ for (let i = 0; i < pIndices.length; i++)
257
+ {
258
+ let tmpCI = pIndices[i];
259
+ tmpHtml += /*html*/`
260
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
261
+ <span style="font-family:monospace; font-size:0.85em; min-width:180px; color:var(--facto-text);">${tmpCI.Dimension}</span>
262
+ ${this._renderBar(tmpCI.CertaintyValue, 150)}
263
+ <span style="font-size:0.85em; font-weight:600; color:${this._certaintyColor(tmpCI.CertaintyValue)};">${(tmpCI.CertaintyValue * 100).toFixed(1)}%</span>
264
+ </div>`;
265
+ }
266
+
267
+ tmpHtml += '</div>';
268
+ return tmpHtml;
269
+ }
270
+
271
+ _renderBar(pValue, pWidth)
272
+ {
273
+ let tmpPct = Math.max(0, Math.min(1, pValue)) * 100;
274
+ let tmpColor = this._certaintyColor(pValue);
275
+ let tmpBgColor = 'var(--facto-bg-elevated)';
276
+ return '<div style="display:inline-block; width:' + pWidth + 'px; height:14px; background:' + tmpBgColor + '; border-radius:3px; overflow:hidden; vertical-align:middle;">'
277
+ + '<div style="width:' + tmpPct + '%; height:100%; background:' + tmpColor + '; border-radius:3px; transition:width 0.3s;"></div>'
278
+ + '</div>';
279
+ }
280
+
281
+ _certaintyColor(pValue)
282
+ {
283
+ if (pValue >= 0.7) return 'var(--facto-success)';
284
+ if (pValue >= 0.4) return 'var(--facto-warning)';
285
+ return 'var(--facto-error)';
286
+ }
287
+ }
288
+
289
+ module.exports = FactoRecordsView;
290
+
291
+ module.exports.default_configuration =
292
+ {
293
+ ViewIdentifier: 'Facto-Records',
294
+ DefaultRenderable: 'Facto-Records',
295
+ DefaultDestinationAddress: '#Facto-Section-Records',
296
+ Templates:
297
+ [
298
+ {
299
+ Hash: 'Facto-Records',
300
+ Template: /*html*/`
301
+ <div class="accordion-row">
302
+ <div class="accordion-number">3</div>
303
+ <div class="accordion-card" id="facto-card-records">
304
+ <div class="accordion-header" onclick="pict.views['Facto-Layout'].toggleSection('facto-card-records')">
305
+ <span class="accordion-title">Records</span>
306
+ <span class="accordion-preview">Browse ingested records</span>
307
+ <span class="accordion-toggle">&#9660;</span>
308
+ </div>
309
+ <div class="accordion-body">
310
+ <p style="margin-bottom:12px; color:#666; font-size:0.9em;">Individual data records with versioning, certainty indices, and temporal metadata.</p>
311
+ <div id="facto-records-list"></div>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ `
316
+ }
317
+ ],
318
+ Renderables:
319
+ [
320
+ {
321
+ RenderableHash: 'Facto-Records',
322
+ TemplateHash: 'Facto-Records',
323
+ DestinationAddress: '#Facto-Section-Records'
324
+ }
325
+ ]
326
+ };