fraim-hub 2.0.339 → 2.0.340
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/registry/scripts/learning/completion-state.js +153 -0
- package/dist/src/ai-hub/desktop-main.js +46 -0
- package/dist/src/ai-hub/hosts.js +18 -0
- package/dist/src/ai-hub/server.js +67 -69
- package/dist/src/core/handoff-contracts.js +617 -0
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +36 -9
- package/dist/src/local-mcp-server/learning-context-builder.js +96 -19
- package/dist/src/local-mcp-server/learning-usage-events.js +63 -57
- package/dist/src/local-mcp-server/learning-usage-store.js +86 -55
- package/package.json +4 -2
- package/public/ai-hub/review.css +378 -0
- package/public/ai-hub/script.js +763 -33
- package/public/ai-hub/styles.css +53 -0
|
@@ -10,6 +10,7 @@ exports.ruleUsageKey = ruleUsageKey;
|
|
|
10
10
|
exports.resolveEntryIdentity = resolveEntryIdentity;
|
|
11
11
|
exports.normalizeModelName = normalizeModelName;
|
|
12
12
|
exports.readUsageSnapshot = readUsageSnapshot;
|
|
13
|
+
exports.resolveUsageCheckpointPath = resolveUsageCheckpointPath;
|
|
13
14
|
exports.writeUsageSnapshot = writeUsageSnapshot;
|
|
14
15
|
exports.foldUsageEvents = foldUsageEvents;
|
|
15
16
|
exports.readUsageStore = readUsageStore;
|
|
@@ -46,18 +47,10 @@ exports.compactUsageEvents = compactUsageEvents;
|
|
|
46
47
|
* (`src/ai-hub/server.ts`). That placement is what satisfies R18; there is no
|
|
47
48
|
* second guardrail here.
|
|
48
49
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* ## Issue #1752 — what changed and why
|
|
54
|
-
*
|
|
55
|
-
* The durable artifact is now the append-only event log in
|
|
56
|
-
* `learning-usage-events.ts`; `usage/learning-usage.json` is a derived snapshot
|
|
57
|
-
* carrying a `foldedThroughBytes` checkpoint into it. The old read-modify-write
|
|
58
|
-
* lost 1589 of 1600 writes from eight concurrent processes on a plain local disk
|
|
59
|
-
* (spike S1b), which is the real cause of both the impossible `fired > offered`
|
|
60
|
-
* counts and the four OneDrive conflict copies.
|
|
50
|
+
* Sleep consolidates the append-only journal into learning-usage.json and its
|
|
51
|
+
* recoverable .checkpoint copy before clearing processed events. Those snapshots
|
|
52
|
+
* preserve lifetime aggregates and source bookmarks after journal cleanup.
|
|
53
|
+
* Raw retrospectives preserve lessons and attestations, not every runtime offer.
|
|
61
54
|
*
|
|
62
55
|
* Four smaller corrections ride with it, each removing a place where the record
|
|
63
56
|
* gave a confident answer it could not prove:
|
|
@@ -306,32 +299,45 @@ function readCheckpoints(value) {
|
|
|
306
299
|
}
|
|
307
300
|
return out;
|
|
308
301
|
}
|
|
309
|
-
/**
|
|
310
|
-
* Parse the snapshot alone, with no event folding. A missing or corrupt snapshot
|
|
311
|
-
* reads as empty: it is derived data, so losing it is recoverable and must never
|
|
312
|
-
* break the loader that reads it.
|
|
313
|
-
*/
|
|
302
|
+
/** Read the normal snapshot, recovering a missing or torn copy from its checkpoint. */
|
|
314
303
|
function readUsageSnapshot(storePath = resolveUsageStorePath()) {
|
|
304
|
+
for (const candidate of [storePath, resolveUsageCheckpointPath(storePath)]) {
|
|
305
|
+
const snapshot = parseUsageSnapshot(candidate);
|
|
306
|
+
if (snapshot)
|
|
307
|
+
return snapshot;
|
|
308
|
+
}
|
|
309
|
+
if ((0, fs_1.existsSync)(resolveUsageCheckpointPath(storePath))) {
|
|
310
|
+
throw new Error('Usage snapshot and checkpoint backup are unreadable. Restore a matching backup; do not delete the usage directory.');
|
|
311
|
+
}
|
|
312
|
+
return emptyStore();
|
|
313
|
+
}
|
|
314
|
+
function resolveUsageCheckpointPath(storePath) {
|
|
315
|
+
return storePath + '.checkpoint';
|
|
316
|
+
}
|
|
317
|
+
function parseUsageSnapshot(storePath) {
|
|
315
318
|
try {
|
|
316
319
|
if (!(0, fs_1.existsSync)(storePath))
|
|
317
|
-
return
|
|
320
|
+
return null;
|
|
318
321
|
const parsed = JSON.parse((0, fs_1.readFileSync)(storePath, 'utf8'));
|
|
319
322
|
// `typeof [] === 'object'`, so an array has to be rejected explicitly: otherwise
|
|
320
323
|
// Object.values yields whatever the array held and every consumer that reads
|
|
321
324
|
// `entry.offered` throws on it.
|
|
322
325
|
if (!parsed || typeof parsed !== 'object' || !parsed.entries
|
|
323
326
|
|| typeof parsed.entries !== 'object' || Array.isArray(parsed.entries)) {
|
|
324
|
-
return
|
|
327
|
+
return null;
|
|
325
328
|
}
|
|
326
329
|
// Drop any value that is not a usable record rather than handing a caller
|
|
327
330
|
// something it will crash on. A partially corrupt store degrades to the records
|
|
328
|
-
// that are still readable
|
|
331
|
+
// that are still readable before consolidation. Consolidated snapshots must be complete.
|
|
329
332
|
const entries = {};
|
|
330
333
|
for (const [key, value] of Object.entries(parsed.entries)) {
|
|
331
334
|
if (isUsableRecord(value))
|
|
332
335
|
entries[key] = value;
|
|
336
|
+
else if (parsed.consolidated)
|
|
337
|
+
return null;
|
|
333
338
|
}
|
|
334
339
|
return {
|
|
340
|
+
consolidated: parsed.consolidated === true,
|
|
335
341
|
version: parsed.version ?? STORE_VERSION,
|
|
336
342
|
updatedAt: parsed.updatedAt ?? emptyStore().updatedAt,
|
|
337
343
|
entries,
|
|
@@ -342,28 +348,18 @@ function readUsageSnapshot(storePath = resolveUsageStorePath()) {
|
|
|
342
348
|
};
|
|
343
349
|
}
|
|
344
350
|
catch {
|
|
345
|
-
return
|
|
351
|
+
return null;
|
|
346
352
|
}
|
|
347
353
|
}
|
|
348
354
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* rename over a target any process holds open failing EPERM 100 times in 100, on
|
|
353
|
-
* both a synced and a local path, because that is Windows file-sharing semantics
|
|
354
|
-
* rather than a OneDrive artefact. The usage store is read by concurrent MCP
|
|
355
|
-
* processes by design, so a rename-published snapshot would fail essentially
|
|
356
|
-
* always in production. This is safe precisely because the snapshot is derived: a
|
|
357
|
-
* torn one is rejected by `readUsageSnapshot` and rebuilt from the log.
|
|
358
|
-
*
|
|
359
|
-
* Returns whether it landed. A failure is never thrown: the log is authoritative
|
|
360
|
-
* and the next reader either uses the older snapshot with a longer tail, or folds
|
|
361
|
-
* cold.
|
|
355
|
+
* Refresh the normal snapshot in place (Windows readers can prevent rename).
|
|
356
|
+
* Sleep alone publishes the separate flushed checkpoint backup before clearing
|
|
357
|
+
* journals, so a torn ordinary refresh can recover without replaying old history.
|
|
362
358
|
*/
|
|
363
359
|
function writeUsageSnapshot(store, storePath = resolveUsageStorePath()) {
|
|
364
360
|
try {
|
|
365
361
|
(0, fs_1.mkdirSync)((0, path_1.dirname)(storePath), { recursive: true });
|
|
366
|
-
(0, fs_1.writeFileSync)(storePath, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
|
|
362
|
+
(0, fs_1.writeFileSync)(storePath, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf8', flush: true });
|
|
367
363
|
return true;
|
|
368
364
|
}
|
|
369
365
|
catch {
|
|
@@ -385,6 +381,8 @@ function foldUsageEvents(options) {
|
|
|
385
381
|
// A checkpoint past the end of its log means the log was compacted or replaced
|
|
386
382
|
// under this snapshot, so the snapshot may already count events the log no
|
|
387
383
|
// longer holds. Re-folding onto it would double count. Discard and fold cold.
|
|
384
|
+
if (tail.restarted && base.consolidated)
|
|
385
|
+
throw new Error('Usage journal no longer matches its consolidated snapshot. Restore matching checkpoint files before continuing.');
|
|
388
386
|
if (tail.restarted) {
|
|
389
387
|
const cold = (0, learning_usage_events_1.readEventsSince)(options.eventDir, {});
|
|
390
388
|
return applyTail(emptyStore(), cold, limits, now, true);
|
|
@@ -393,6 +391,7 @@ function foldUsageEvents(options) {
|
|
|
393
391
|
}
|
|
394
392
|
function applyTail(base, tail, limits, now, restarted) {
|
|
395
393
|
const store = {
|
|
394
|
+
consolidated: base.consolidated,
|
|
396
395
|
version: base.version,
|
|
397
396
|
updatedAt: base.updatedAt,
|
|
398
397
|
entries: base.entries,
|
|
@@ -430,13 +429,23 @@ function readUsageStore(storePath = resolveUsageStorePath(), options = {}) {
|
|
|
430
429
|
const eventDir = options.eventDir ?? (0, learning_usage_events_1.resolveEventDir)(storePath);
|
|
431
430
|
if ((0, learning_usage_events_1.listEventLogs)(eventDir).length === 0)
|
|
432
431
|
return snapshot;
|
|
433
|
-
const
|
|
432
|
+
const fold = (base) => foldUsageEvents({
|
|
434
433
|
eventDir,
|
|
435
434
|
limits: options.limits,
|
|
436
435
|
now: options.now,
|
|
437
|
-
base
|
|
438
|
-
checkpoints:
|
|
436
|
+
base,
|
|
437
|
+
checkpoints: base.foldedThroughBytes ?? {},
|
|
439
438
|
});
|
|
439
|
+
let folded;
|
|
440
|
+
try {
|
|
441
|
+
folded = fold(snapshot);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
const backup = parseUsageSnapshot(resolveUsageCheckpointPath(storePath));
|
|
445
|
+
if (!backup)
|
|
446
|
+
throw error;
|
|
447
|
+
folded = fold(backup);
|
|
448
|
+
}
|
|
440
449
|
const mode = options.refreshSnapshot ?? 'auto';
|
|
441
450
|
const shouldRefresh = mode === 'always'
|
|
442
451
|
|| (mode === 'auto' && (folded.tailBytes >= SNAPSHOT_REFRESH_BYTES || !(0, fs_1.existsSync)(storePath) || folded.restarted));
|
|
@@ -747,7 +756,7 @@ function flagIntegrity(store) {
|
|
|
747
756
|
/**
|
|
748
757
|
* Materialize the snapshot when there is none.
|
|
749
758
|
*
|
|
750
|
-
*
|
|
759
|
+
* Before the first consolidation, readers publish snapshots, but a workspace that
|
|
751
760
|
* has only ever been written to would have no snapshot at all, so every reader
|
|
752
761
|
* would pay a cold fold and anything inspecting the store as a file would find
|
|
753
762
|
* nothing there. Bounded: it only runs when the file is absent, and at that point
|
|
@@ -761,7 +770,7 @@ function ensureSnapshotExists(storePath, eventDir, limits, now) {
|
|
|
761
770
|
readUsageStore(storePath, { limits, now, eventDir, refreshSnapshot: 'always' });
|
|
762
771
|
}
|
|
763
772
|
catch {
|
|
764
|
-
//
|
|
773
|
+
// Initial cache publication can retry; consolidated history has a separate backup.
|
|
765
774
|
}
|
|
766
775
|
}
|
|
767
776
|
/**
|
|
@@ -899,26 +908,48 @@ function pruneUsageStore(limits, options = {}) {
|
|
|
899
908
|
const compaction = compactUsageEvents({ eventDir, snapshotPath: storePath, limits, now, store });
|
|
900
909
|
return { prunedOfferRecords, prunedFiringRecords, entries: Object.keys(store.entries).length, compaction };
|
|
901
910
|
}
|
|
902
|
-
/**
|
|
903
|
-
*
|
|
904
|
-
* snapshot's checkpoint onto the rewritten logs.
|
|
905
|
-
*
|
|
906
|
-
* Re-basing is not optional: leaving a checkpoint pointing into the old log would
|
|
907
|
-
* make the next reader either re-count events the snapshot already holds, or skip
|
|
908
|
-
* ones it does not.
|
|
911
|
+
/** Save totals and source bookmarks in both copies, then clear processed journals.
|
|
912
|
+
* Requires an interval without concurrent writers or sync activity.
|
|
909
913
|
*/
|
|
910
914
|
function compactUsageEvents(options) {
|
|
911
915
|
const now = options.now ?? new Date();
|
|
912
916
|
const store = options.store
|
|
913
917
|
?? readUsageStore(options.snapshotPath, { limits: options.limits, now, eventDir: options.eventDir });
|
|
914
|
-
const
|
|
918
|
+
for (const log of (0, learning_usage_events_1.listEventLogs)(options.eventDir)) {
|
|
919
|
+
if ((store.foldedThroughBytes?.[log.machineId] ?? 0) < log.baseOffset) {
|
|
920
|
+
throw new Error('Cannot consolidate from a snapshot older than the journal checkpoint.');
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
for (const entry of Object.values(store.entries))
|
|
924
|
+
trimLogs(entry, options.limits, now);
|
|
925
|
+
store.consolidated = true;
|
|
926
|
+
store.updatedAt = now.toISOString();
|
|
927
|
+
// Publish the recoverable checkpoint first, then the normal snapshot. No
|
|
928
|
+
// journal is cleared unless both writes succeed. The backup is never written
|
|
929
|
+
// by ordinary readers, so a torn cache refresh cannot destroy both copies.
|
|
930
|
+
const checkpoint = resolveUsageCheckpointPath(options.snapshotPath);
|
|
931
|
+
const temp = checkpoint + '.tmp';
|
|
932
|
+
try {
|
|
933
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(checkpoint), { recursive: true });
|
|
934
|
+
(0, fs_1.writeFileSync)(temp, JSON.stringify(store, null, 2) + '\n', { encoding: 'utf8', flush: true });
|
|
935
|
+
(0, fs_1.renameSync)(temp, checkpoint);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
try {
|
|
939
|
+
(0, fs_1.rmSync)(temp, { force: true });
|
|
940
|
+
}
|
|
941
|
+
catch { /* Best effort. */ }
|
|
942
|
+
throw new Error('Cannot consolidate usage events: checkpoint backup could not be saved.');
|
|
943
|
+
}
|
|
944
|
+
if (!writeUsageSnapshot(store, options.snapshotPath)) {
|
|
945
|
+
throw new Error('Cannot consolidate usage events: aggregate snapshot could not be saved.');
|
|
946
|
+
}
|
|
947
|
+
return (0, learning_usage_events_1.compactEventLogs)(options.eventDir, (event) => {
|
|
948
|
+
if (event.kind !== 'offer' && event.kind !== 'firing')
|
|
949
|
+
return true;
|
|
915
950
|
const date = typeof event.date === 'string' ? event.date : '';
|
|
916
|
-
if (!isValidDate(date))
|
|
951
|
+
if (typeof event.key !== 'string' || !event.key || !isValidDate(date))
|
|
917
952
|
return true;
|
|
918
|
-
return
|
|
919
|
-
});
|
|
920
|
-
store.foldedThroughBytes = Object.fromEntries(result.logs.map((log) => [log.machineId, log.size]));
|
|
921
|
-
store.updatedAt = now.toISOString();
|
|
922
|
-
writeUsageSnapshot(store, options.snapshotPath);
|
|
923
|
-
return result;
|
|
953
|
+
return event.kind === 'firing' && !exports.FIRING_OUTCOMES.includes(event.outcome);
|
|
954
|
+
}, store.foldedThroughBytes ?? {});
|
|
924
955
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.340",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"author": "Sid Mathur <sid.mathur@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/mathursrus/FRAIM#readme",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"dist/registry/scripts/learning/learning-domains.json",
|
|
20
20
|
"dist/registry/scripts/learning/entry-format.js",
|
|
21
|
+
"dist/registry/scripts/learning/completion-state.js",
|
|
21
22
|
"dist/src/ai-hub/",
|
|
22
23
|
"dist/src/api/admin/payments.js",
|
|
23
24
|
"dist/src/api/admin/sales-leads.js",
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"dist/src/config/stripe.js",
|
|
73
74
|
"dist/src/core/brand-store.js",
|
|
74
75
|
"dist/src/core/capability-pack.js",
|
|
76
|
+
"dist/src/core/handoff-contracts.js",
|
|
75
77
|
"dist/src/core/quality-evidence.js",
|
|
76
78
|
"dist/src/core/job-visualization.js",
|
|
77
79
|
"dist/src/core/resolve-phase-edge.js",
|
|
@@ -226,7 +228,7 @@
|
|
|
226
228
|
"electron-updater": "^6.8.9",
|
|
227
229
|
"express": "^5.2.1",
|
|
228
230
|
"extract-zip": "^2.0.1",
|
|
229
|
-
"fraim": "2.0.
|
|
231
|
+
"fraim": "2.0.340",
|
|
230
232
|
"mongodb": "^7.0.0",
|
|
231
233
|
"node-cron": "4.2.1",
|
|
232
234
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/review.css
CHANGED
|
@@ -634,3 +634,381 @@
|
|
|
634
634
|
|
|
635
635
|
.review-turn .review-actions { border: 0; margin: 0; padding: 0; }
|
|
636
636
|
.review-turn .rb-handoff { flex-basis: 100%; }
|
|
637
|
+
|
|
638
|
+
/* ===========================================================================
|
|
639
|
+
Issue #1755 - the learnings library.
|
|
640
|
+
The corpus was a flat list of full-text cards with no summary, no filter and
|
|
641
|
+
no sign of whether an entry has ever worked. These rules add the stats row,
|
|
642
|
+
the filter toolbar, the domain grouping, and the per-card strength + usage
|
|
643
|
+
footer. Every colour comes from the existing token set.
|
|
644
|
+
=========================================================================== */
|
|
645
|
+
|
|
646
|
+
/* Stats row */
|
|
647
|
+
.ll-stats {
|
|
648
|
+
display: flex;
|
|
649
|
+
align-items: center;
|
|
650
|
+
flex-wrap: wrap;
|
|
651
|
+
gap: 6px 14px;
|
|
652
|
+
padding: 2px 2px 12px;
|
|
653
|
+
}
|
|
654
|
+
.ll-stats .ll-stat {
|
|
655
|
+
display: inline-flex;
|
|
656
|
+
align-items: baseline;
|
|
657
|
+
gap: 5px;
|
|
658
|
+
font-size: 11.5px;
|
|
659
|
+
color: var(--muted);
|
|
660
|
+
white-space: nowrap;
|
|
661
|
+
}
|
|
662
|
+
.ll-stats .ll-stat b { font-size: 14px; font-weight: 700; color: var(--text); }
|
|
663
|
+
.ll-stats .ll-total b { font-size: 16px; }
|
|
664
|
+
.ll-stats .ll-stat::before {
|
|
665
|
+
content: '';
|
|
666
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
667
|
+
background: var(--muted);
|
|
668
|
+
align-self: center;
|
|
669
|
+
}
|
|
670
|
+
.ll-stats .ll-total::before { display: none; }
|
|
671
|
+
.ll-stats .s-strong::before { background: var(--done); }
|
|
672
|
+
.ll-stats .s-wouldfire::before { background: var(--warn); }
|
|
673
|
+
.ll-stats .s-unresolved::before { background: var(--strength-unresolved); }
|
|
674
|
+
.ll-stats .s-weak::before { background: var(--line); box-shadow: inset 0 0 0 1px var(--muted); }
|
|
675
|
+
.ll-stats .s-dead::before { background: transparent; box-shadow: inset 0 0 0 1px var(--line); }
|
|
676
|
+
|
|
677
|
+
/* Filter + sort toolbar */
|
|
678
|
+
.ll-tools {
|
|
679
|
+
display: flex;
|
|
680
|
+
align-items: center;
|
|
681
|
+
justify-content: space-between;
|
|
682
|
+
flex-wrap: wrap;
|
|
683
|
+
gap: 8px;
|
|
684
|
+
padding-bottom: 12px;
|
|
685
|
+
}
|
|
686
|
+
.ll-tools .ll-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
687
|
+
.ll-tools .ll-tab {
|
|
688
|
+
font: inherit;
|
|
689
|
+
font-size: 11.5px;
|
|
690
|
+
font-weight: 600;
|
|
691
|
+
color: var(--muted);
|
|
692
|
+
background: var(--surface);
|
|
693
|
+
border: 1px solid var(--line);
|
|
694
|
+
border-radius: 999px;
|
|
695
|
+
padding: 4px 11px;
|
|
696
|
+
cursor: pointer;
|
|
697
|
+
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
|
698
|
+
}
|
|
699
|
+
.ll-tools .ll-tab:hover { color: var(--text); border-color: var(--muted); }
|
|
700
|
+
.ll-tools .ll-tab.active {
|
|
701
|
+
color: var(--accent-strong);
|
|
702
|
+
background: var(--accent-soft);
|
|
703
|
+
border-color: var(--accent);
|
|
704
|
+
}
|
|
705
|
+
.ll-tools .ll-selects { display: flex; gap: 8px; }
|
|
706
|
+
.ll-tools .ll-sel {
|
|
707
|
+
font: inherit;
|
|
708
|
+
font-size: 11.5px;
|
|
709
|
+
color: var(--text);
|
|
710
|
+
background: var(--surface);
|
|
711
|
+
border: 1px solid var(--line);
|
|
712
|
+
border-radius: 8px;
|
|
713
|
+
padding: 4px 8px;
|
|
714
|
+
cursor: pointer;
|
|
715
|
+
max-width: 190px;
|
|
716
|
+
}
|
|
717
|
+
.ll-tools .ll-sel:focus { outline: none; border-color: var(--accent); }
|
|
718
|
+
|
|
719
|
+
/* Domain group header */
|
|
720
|
+
.ll-group-head {
|
|
721
|
+
display: flex;
|
|
722
|
+
align-items: baseline;
|
|
723
|
+
gap: 8px;
|
|
724
|
+
margin-top: 8px;
|
|
725
|
+
padding-bottom: 5px;
|
|
726
|
+
border-bottom: 1px solid var(--line);
|
|
727
|
+
}
|
|
728
|
+
.learn-list > .ll-group-head:first-child { margin-top: 0; }
|
|
729
|
+
.ll-group-head .llg-name {
|
|
730
|
+
font-size: 10.5px;
|
|
731
|
+
font-weight: 700;
|
|
732
|
+
letter-spacing: 0.06em;
|
|
733
|
+
text-transform: uppercase;
|
|
734
|
+
color: var(--text);
|
|
735
|
+
}
|
|
736
|
+
.ll-group-head .llg-reach { font-size: 11px; color: var(--muted); }
|
|
737
|
+
.ll-group-head .llg-count {
|
|
738
|
+
margin-left: auto;
|
|
739
|
+
font-size: 11px;
|
|
740
|
+
font-weight: 600;
|
|
741
|
+
color: var(--muted);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/* No-match state */
|
|
745
|
+
.ll-nomatch { font-size: 13px; color: var(--muted); padding: 18px 2px 4px; }
|
|
746
|
+
|
|
747
|
+
/* Card: domain, strength and score badges */
|
|
748
|
+
.learn-prop .lp-domain {
|
|
749
|
+
font-size: 10px;
|
|
750
|
+
font-weight: 700;
|
|
751
|
+
padding: 2px 8px;
|
|
752
|
+
border-radius: 6px;
|
|
753
|
+
letter-spacing: 0.03em;
|
|
754
|
+
flex-shrink: 0;
|
|
755
|
+
background: var(--bg);
|
|
756
|
+
color: var(--muted);
|
|
757
|
+
box-shadow: inset 0 0 0 1px var(--line);
|
|
758
|
+
}
|
|
759
|
+
.learn-prop .lp-domain.dom-global { color: var(--muted); }
|
|
760
|
+
.learn-prop .lp-domain.dom-c0 { color: var(--domain-slot-0); box-shadow: inset 0 0 0 1px var(--domain-slot-0-ring); }
|
|
761
|
+
.learn-prop .lp-domain.dom-c1 { color: var(--domain-slot-1); box-shadow: inset 0 0 0 1px var(--domain-slot-1-ring); }
|
|
762
|
+
.learn-prop .lp-domain.dom-c2 { color: var(--domain-slot-2); box-shadow: inset 0 0 0 1px var(--domain-slot-2-ring); }
|
|
763
|
+
.learn-prop .lp-domain.dom-c3 { color: var(--domain-slot-3); box-shadow: inset 0 0 0 1px var(--domain-slot-3-ring); }
|
|
764
|
+
.learn-prop .lp-domain.dom-c4 { color: var(--domain-slot-4); box-shadow: inset 0 0 0 1px var(--domain-slot-4-ring); }
|
|
765
|
+
.learn-prop .lp-domain.dom-c5 { color: var(--domain-slot-5); box-shadow: inset 0 0 0 1px var(--domain-slot-5-ring); }
|
|
766
|
+
|
|
767
|
+
.learn-prop .lp-strength {
|
|
768
|
+
display: inline-flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
gap: 5px;
|
|
771
|
+
font-size: 10.5px;
|
|
772
|
+
font-weight: 700;
|
|
773
|
+
padding: 2px 8px 2px 6px;
|
|
774
|
+
border-radius: 6px;
|
|
775
|
+
flex-shrink: 0;
|
|
776
|
+
background: var(--bg);
|
|
777
|
+
color: var(--muted);
|
|
778
|
+
}
|
|
779
|
+
.learn-prop .lp-strength .lp-dot {
|
|
780
|
+
width: 6px; height: 6px; border-radius: 50%; background: currentColor;
|
|
781
|
+
flex-shrink: 0;
|
|
782
|
+
}
|
|
783
|
+
.learn-prop .lp-strength.s-strong { color: var(--done); }
|
|
784
|
+
.learn-prop .lp-strength.s-wouldfire { color: var(--warn); }
|
|
785
|
+
.learn-prop .lp-strength.s-unresolved { color: var(--strength-unresolved); }
|
|
786
|
+
.learn-prop .lp-strength.s-weak { color: var(--muted); }
|
|
787
|
+
.learn-prop .lp-strength.s-dead { color: var(--muted); opacity: 0.65; }
|
|
788
|
+
.learn-prop .lp-strength.s-dead .lp-dot { background: transparent; box-shadow: inset 0 0 0 1px currentColor; }
|
|
789
|
+
|
|
790
|
+
.learn-prop .lp-score {
|
|
791
|
+
font-size: 11px;
|
|
792
|
+
font-weight: 700;
|
|
793
|
+
font-variant-numeric: tabular-nums;
|
|
794
|
+
color: var(--muted);
|
|
795
|
+
background: var(--bg);
|
|
796
|
+
border-radius: 6px;
|
|
797
|
+
padding: 2px 7px;
|
|
798
|
+
flex-shrink: 0;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/* Card body clamp */
|
|
802
|
+
.learn-prop .lp-body.lp-clamped {
|
|
803
|
+
display: -webkit-box;
|
|
804
|
+
-webkit-line-clamp: 3;
|
|
805
|
+
line-clamp: 3;
|
|
806
|
+
-webkit-box-orient: vertical;
|
|
807
|
+
overflow: hidden;
|
|
808
|
+
}
|
|
809
|
+
.learn-prop .lp-more {
|
|
810
|
+
align-self: flex-start;
|
|
811
|
+
font: inherit;
|
|
812
|
+
font-size: 11.5px;
|
|
813
|
+
font-weight: 600;
|
|
814
|
+
color: var(--accent);
|
|
815
|
+
background: none;
|
|
816
|
+
border: none;
|
|
817
|
+
padding: 0;
|
|
818
|
+
margin-top: -2px;
|
|
819
|
+
cursor: pointer;
|
|
820
|
+
}
|
|
821
|
+
.learn-prop .lp-more:hover { color: var(--accent-strong); text-decoration: underline; }
|
|
822
|
+
|
|
823
|
+
/* Card usage footer */
|
|
824
|
+
.learn-prop .lp-stats {
|
|
825
|
+
display: flex;
|
|
826
|
+
align-items: center;
|
|
827
|
+
flex-wrap: wrap;
|
|
828
|
+
gap: 6px 12px;
|
|
829
|
+
margin-top: 2px;
|
|
830
|
+
padding-top: 9px;
|
|
831
|
+
border-top: 1px solid var(--line);
|
|
832
|
+
font-size: 11px;
|
|
833
|
+
color: var(--muted);
|
|
834
|
+
}
|
|
835
|
+
.learn-prop .lp-stats .lps-none { font-style: italic; }
|
|
836
|
+
.learn-prop .lp-bar {
|
|
837
|
+
display: flex;
|
|
838
|
+
width: 68px;
|
|
839
|
+
height: 4px;
|
|
840
|
+
border-radius: 999px;
|
|
841
|
+
overflow: hidden;
|
|
842
|
+
background: var(--line);
|
|
843
|
+
flex-shrink: 0;
|
|
844
|
+
}
|
|
845
|
+
.learn-prop .lp-bar i { display: block; height: 100%; }
|
|
846
|
+
.learn-prop .lp-bar .lb-applied { background: var(--done); }
|
|
847
|
+
.learn-prop .lp-bar .lb-prevented { background: var(--accent); }
|
|
848
|
+
.learn-prop .lp-bar .lb-ignored { background: var(--warn); }
|
|
849
|
+
.learn-prop .lp-stats .lps-counts { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
850
|
+
.learn-prop .lp-stats .lps-c { display: inline-flex; align-items: baseline; gap: 4px; }
|
|
851
|
+
.learn-prop .lp-stats .lps-c b { font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
|
|
852
|
+
.learn-prop .lp-stats .lps-last { margin-left: auto; white-space: nowrap; }
|
|
853
|
+
|
|
854
|
+
/* Domain select in the inline editor */
|
|
855
|
+
.learn-prop .lp-edit-domain {
|
|
856
|
+
font: inherit; font-size: 12px; padding: 6px 8px; border: 1px solid var(--line);
|
|
857
|
+
border-radius: 8px; background: var(--surface); color: var(--text);
|
|
858
|
+
}
|
|
859
|
+
.learn-prop .lp-edit-domain:focus { outline: none; border-color: var(--accent); }
|
|
860
|
+
|
|
861
|
+
/* Collapsed sections (below-threshold + archived). These had no rules at all,
|
|
862
|
+
so they rendered as an unstyled details block in the middle of a card list. */
|
|
863
|
+
.learn-dormant-section {
|
|
864
|
+
border: 1px dashed var(--line);
|
|
865
|
+
border-radius: 14px;
|
|
866
|
+
padding: 4px 12px;
|
|
867
|
+
background: transparent;
|
|
868
|
+
}
|
|
869
|
+
.learn-dormant-section > .learn-dormant-header {
|
|
870
|
+
font-size: 11.5px;
|
|
871
|
+
font-weight: 600;
|
|
872
|
+
color: var(--muted);
|
|
873
|
+
padding: 8px 2px;
|
|
874
|
+
cursor: pointer;
|
|
875
|
+
list-style: none;
|
|
876
|
+
}
|
|
877
|
+
.learn-dormant-section > .learn-dormant-header::-webkit-details-marker { display: none; }
|
|
878
|
+
.learn-dormant-section > .learn-dormant-header::before {
|
|
879
|
+
content: '\25B8';
|
|
880
|
+
display: inline-block;
|
|
881
|
+
width: 12px;
|
|
882
|
+
transition: transform 140ms ease;
|
|
883
|
+
}
|
|
884
|
+
.learn-dormant-section[open] > .learn-dormant-header::before { transform: rotate(90deg); }
|
|
885
|
+
.learn-dormant-section > .learn-prop {
|
|
886
|
+
margin-bottom: 10px;
|
|
887
|
+
background: var(--bg);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
/* Issue #1755: entries are authored as short labelled lines (Takeaway / Why this
|
|
892
|
+
matters / Example / What to do next). Collapsing them into one paragraph is
|
|
893
|
+
what made the readable ones read like the dense ones. */
|
|
894
|
+
.learn-prop .lp-body { white-space: pre-line; }
|
|
895
|
+
|
|
896
|
+
/* The provenance tail, revealed with the rest of the entry. */
|
|
897
|
+
.learn-prop .lp-prov {
|
|
898
|
+
white-space: pre-line;
|
|
899
|
+
font-size: 11.5px;
|
|
900
|
+
line-height: 1.55;
|
|
901
|
+
color: var(--muted);
|
|
902
|
+
border-left: 2px solid var(--line);
|
|
903
|
+
padding-left: 10px;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/* Issue #1755: the accordion body is a capped scroll region (.ctx-acc-body), so
|
|
907
|
+
the library's summary and controls pin to its top rather than scrolling out of
|
|
908
|
+
reach behind a hundred cards. */
|
|
909
|
+
.ll-head {
|
|
910
|
+
position: sticky;
|
|
911
|
+
top: -4px;
|
|
912
|
+
z-index: 2;
|
|
913
|
+
background: var(--ll-head-bg);
|
|
914
|
+
margin: -4px -2px 0;
|
|
915
|
+
padding: 6px 2px 0;
|
|
916
|
+
box-shadow: var(--ll-head-shadow);
|
|
917
|
+
}
|
|
918
|
+
.ll-head .ll-tools { padding-bottom: 10px; }
|
|
919
|
+
|
|
920
|
+
/* Issue #1755 R6: the proposal card's two zones. Configuration sits in a recessed
|
|
921
|
+
block so the controls that change what gets written read as settings, and the
|
|
922
|
+
decision sits below a hairline so it reads as the commit. */
|
|
923
|
+
.learn-prop .lp-config {
|
|
924
|
+
display: flex;
|
|
925
|
+
flex-direction: column;
|
|
926
|
+
gap: 8px;
|
|
927
|
+
background: var(--lp-recessed);
|
|
928
|
+
border-radius: 10px;
|
|
929
|
+
padding: 9px 11px 11px;
|
|
930
|
+
margin-top: 2px;
|
|
931
|
+
}
|
|
932
|
+
.learn-prop .lp-config .lp-config-head {
|
|
933
|
+
font-size: 10px;
|
|
934
|
+
font-weight: 700;
|
|
935
|
+
letter-spacing: 0.06em;
|
|
936
|
+
text-transform: uppercase;
|
|
937
|
+
color: var(--muted);
|
|
938
|
+
}
|
|
939
|
+
.learn-prop .lp-config .lp-actions { margin-top: 0; }
|
|
940
|
+
.learn-prop .lp-decide {
|
|
941
|
+
display: flex;
|
|
942
|
+
align-items: center;
|
|
943
|
+
flex-wrap: wrap;
|
|
944
|
+
gap: 10px;
|
|
945
|
+
margin-top: 2px;
|
|
946
|
+
padding-top: 10px;
|
|
947
|
+
border-top: 1px solid var(--line);
|
|
948
|
+
}
|
|
949
|
+
.learn-prop .lp-decide .lp-scope-note { margin-left: auto; text-align: right; }
|
|
950
|
+
.learn-prop .lp-decided-badge {
|
|
951
|
+
font-size: 10.5px;
|
|
952
|
+
font-weight: 700;
|
|
953
|
+
padding: 2px 9px;
|
|
954
|
+
border-radius: 999px;
|
|
955
|
+
letter-spacing: 0.02em;
|
|
956
|
+
}
|
|
957
|
+
/* Tinted with its own outcome colour rather than --accent-soft: an org brand
|
|
958
|
+
overrides --accent, and a purple-tinted pill around green text reads as two
|
|
959
|
+
unrelated signals. */
|
|
960
|
+
.learn-prop .lp-decided-badge.is-saved { background: var(--bg); color: var(--done); box-shadow: inset 0 0 0 1px currentColor; }
|
|
961
|
+
.learn-prop .lp-decided-badge.is-skipped { background: var(--bg); color: var(--muted); box-shadow: inset 0 0 0 1px var(--line); }
|
|
962
|
+
|
|
963
|
+
/* Issue #1755: at a narrow panel width the head row's badges (domain, category,
|
|
964
|
+
priority, strength) plus the score chip outrun the row, and an unbreakable
|
|
965
|
+
title word pushes the card into horizontal overflow. Let the row wrap and let
|
|
966
|
+
a long title break, rather than letting the card scroll sideways. */
|
|
967
|
+
.learn-prop .lp-head { flex-wrap: wrap; row-gap: 6px; }
|
|
968
|
+
/* A 220px basis is the hinge: while that much room is left after the badges the
|
|
969
|
+
title stays on their line, and below it the title takes a full row of its own
|
|
970
|
+
instead of being squeezed into a four-word-wide column. */
|
|
971
|
+
.learn-prop .lp-head .lp-title { flex: 1 1 220px; overflow-wrap: break-word; }
|
|
972
|
+
|
|
973
|
+
/* Issue #1755: the row actions were display:none until hover, which takes them out
|
|
974
|
+
of the tab order entirely: a keyboard or screen-reader manager could not reach
|
|
975
|
+
Edit or Delete at all, and Edit is now also where an entry is re-scoped. Hide
|
|
976
|
+
them visually instead of structurally, and reveal on focus as well as hover.
|
|
977
|
+
Reserving their space also stops the title reflowing every time the pointer
|
|
978
|
+
crosses a card. */
|
|
979
|
+
.learn-prop .lp-act {
|
|
980
|
+
display: inline-flex;
|
|
981
|
+
opacity: 0;
|
|
982
|
+
transition: opacity 120ms ease;
|
|
983
|
+
}
|
|
984
|
+
.learn-prop:hover .lp-act,
|
|
985
|
+
.learn-prop:focus-within .lp-act,
|
|
986
|
+
.learn-prop .lp-act:focus-visible { opacity: 1; }
|
|
987
|
+
.learn-prop .lp-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
988
|
+
.ll-tools .ll-tab:focus-visible,
|
|
989
|
+
.ll-tools .ll-sel:focus-visible,
|
|
990
|
+
.learn-prop .lp-more:focus-visible {
|
|
991
|
+
outline: 2px solid var(--accent);
|
|
992
|
+
outline-offset: 2px;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/* Issue #1755 R5: the sleep-on-learnings proposal set, rendered in the
|
|
996
|
+
conversation where the job wrote it. Full conversation width, because a
|
|
997
|
+
proposal carries a lesson body, a configuration zone and a decision. */
|
|
998
|
+
.learn-proposal-turn .bubble {
|
|
999
|
+
display: flex;
|
|
1000
|
+
flex-direction: column;
|
|
1001
|
+
gap: 10px;
|
|
1002
|
+
width: 100%;
|
|
1003
|
+
max-width: 100%;
|
|
1004
|
+
box-sizing: border-box;
|
|
1005
|
+
}
|
|
1006
|
+
.learn-proposal-turn .review-turn-label { margin-bottom: 0; }
|
|
1007
|
+
.learn-proposal-turn .learn-batch {
|
|
1008
|
+
background: var(--lp-raised);
|
|
1009
|
+
border: 1px solid var(--lp-raised-line);
|
|
1010
|
+
border-radius: 12px;
|
|
1011
|
+
padding: 9px 12px;
|
|
1012
|
+
}
|
|
1013
|
+
.learn-proposal-turn .learn-list { gap: 10px; }
|
|
1014
|
+
.learn-proposal-turn .learn-prop { background: var(--surface); }
|