Skip to content

Commit 479139c

Browse files
committed
feat(phase-b): persist context bigrams in NSUserDefaults (capped); add persistence test; minor wiring
1 parent 3174dfa commit 479139c

File tree

2 files changed

+85
-1
lines changed

2 files changed

+85
-1
lines changed

ContextRanking.m

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,84 @@
44
//
55

66
#import "ContextRanking.h"
7+
#import <Foundation/Foundation.h>
78

89
static NSMutableArray<NSString *> *s_recentHistory;
910
static NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSNumber *> *> *s_bigrams; // prev -> (next -> count)
1011
static const NSUInteger kHistoryMax = 8; // small cap to keep things light
12+
static NSString * const kBigramsDefaultsKey = @"ContextRankingBigrams";
13+
static const NSUInteger kPrevMax = 64; // cap number of prev tokens to persist
14+
static const NSUInteger kNextPerPrevMax = 12; // cap next variants per prev
15+
16+
static void ensureStoresLoaded(void) {
17+
if (!s_bigrams) {
18+
NSDictionary *saved = [[NSUserDefaults standardUserDefaults] dictionaryForKey:kBigramsDefaultsKey];
19+
s_bigrams = [[NSMutableDictionary alloc] init];
20+
if ([saved isKindOfClass:[NSDictionary class]]) {
21+
[saved enumerateKeysAndObjectsUsingBlock:^(NSString *prev, NSDictionary *nexts, BOOL *stop) {
22+
if (![prev isKindOfClass:[NSString class]] || ![nexts isKindOfClass:[NSDictionary class]]) return;
23+
NSMutableDictionary<NSString *, NSNumber *> *mutableNexts = [[NSMutableDictionary alloc] init];
24+
[nexts enumerateKeysAndObjectsUsingBlock:^(NSString *nxt, NSNumber *cnt, BOOL *stop2) {
25+
if ([nxt isKindOfClass:[NSString class]] && [cnt isKindOfClass:[NSNumber class]]) {
26+
mutableNexts[nxt] = cnt;
27+
}
28+
}];
29+
s_bigrams[prev] = mutableNexts;
30+
}];
31+
}
32+
}
33+
}
34+
35+
static void persistBigrams(void) {
36+
if (!s_bigrams) return;
37+
// Enforce caps: limit number of prev keys and next variants per prev (by lowest count pruning)
38+
if (s_bigrams.count > kPrevMax) {
39+
NSArray *keys = [s_bigrams allKeys];
40+
NSMutableArray *scored = [NSMutableArray arrayWithCapacity:keys.count];
41+
for (NSString *k in keys) {
42+
NSDictionary *m = s_bigrams[k];
43+
NSInteger sum = 0; for (NSNumber *v in [m allValues]) sum += v.integerValue;
44+
[scored addObject:@{ @"k": k, @"s": @(sum) }];
45+
}
46+
[scored sortUsingComparator:^NSComparisonResult(NSDictionary *a, NSDictionary *b){
47+
return [a[@"s"] integerValue] < [b[@"s"] integerValue] ? NSOrderedAscending : NSOrderedDescending;
48+
}];
49+
for (NSUInteger i = kPrevMax; i < scored.count; i++) {
50+
NSString *drop = scored[i][@"k"];
51+
[s_bigrams removeObjectForKey:drop];
52+
}
53+
}
54+
for (NSString *prev in [s_bigrams allKeys]) {
55+
NSMutableDictionary<NSString *, NSNumber *> *nextMap = s_bigrams[prev];
56+
if (nextMap.count > kNextPerPrevMax) {
57+
NSArray *pairs = [nextMap allKeys];
58+
NSArray *sorted = [pairs sortedArrayUsingComparator:^NSComparisonResult(NSString *a, NSString *b){
59+
NSInteger ca = [nextMap[a] integerValue];
60+
NSInteger cb = [nextMap[b] integerValue];
61+
if (ca == cb) return NSOrderedSame;
62+
return (ca > cb) ? NSOrderedAscending : NSOrderedDescending;
63+
}];
64+
NSSet *keep = [NSSet setWithArray:[sorted subarrayWithRange:NSMakeRange(0, kNextPerPrevMax)]];
65+
for (NSString *key in [nextMap allKeys]) {
66+
if (![keep containsObject:key]) [nextMap removeObjectForKey:key];
67+
}
68+
}
69+
}
70+
NSMutableDictionary *toSave = [NSMutableDictionary dictionaryWithCapacity:s_bigrams.count];
71+
[s_bigrams enumerateKeysAndObjectsUsingBlock:^(NSString *prev, NSDictionary *nexts, BOOL *stop){
72+
toSave[prev] = [nexts copy];
73+
}];
74+
[[NSUserDefaults standardUserDefaults] setObject:toSave forKey:kBigramsDefaultsKey];
75+
[[NSUserDefaults standardUserDefaults] synchronize];
76+
}
1177

1278
@implementation ContextRanking
1379

1480
+ (NSArray<NSString *> *)rankCandidates:(NSArray<NSString *> *)candidates
1581
withHistory:(NSArray<NSString *> * _Nullable)history
1682
{
1783
if (!candidates || candidates.count <= 1) return candidates ?: @[];
84+
ensureStoresLoaded();
1885
// Minimal heuristic: boost candidates observed to follow the last committed token.
1986
NSString *last = (history.count > 0 ? history[0] : nil);
2087
if (!last || !s_bigrams) {
@@ -43,11 +110,11 @@ @implementation ContextRanking
43110
+ (void)recordCommittedToken:(NSString *)token {
44111
if (token.length == 0) return;
45112
@synchronized(self) {
113+
ensureStoresLoaded();
46114
if (!s_recentHistory) s_recentHistory = [[NSMutableArray alloc] init];
47115
// Update bigram counts using previous token (if any)
48116
NSString *prev = (s_recentHistory.count > 0 ? s_recentHistory[0] : nil);
49117
if (prev && token) {
50-
if (!s_bigrams) s_bigrams = [[NSMutableDictionary alloc] init];
51118
NSMutableDictionary<NSString *, NSNumber *> *nextMap = s_bigrams[prev];
52119
if (!nextMap) {
53120
nextMap = [[NSMutableDictionary alloc] init];
@@ -62,6 +129,7 @@ + (void)recordCommittedToken:(NSString *)token {
62129
[s_recentHistory insertObject:token atIndex:0];
63130
while (s_recentHistory.count > kHistoryMax) [s_recentHistory removeLastObject];
64131
}
132+
persistBigrams();
65133
}
66134

67135
+ (NSArray<NSString *> *)recentHistory:(NSUInteger)limit {

Tests/ContextRankingTests.m

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,20 @@ - (void)testBigramBoostMovesSeenPairUp {
3939
XCTAssertEqualObjects(out[0], curr, @"Observed bigram should be boosted to front");
4040
}
4141

42+
- (void)testPersistenceWritesBigramsToUserDefaults {
43+
// Clear any prior data
44+
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"ContextRankingBigrams"];
45+
[[NSUserDefaults standardUserDefaults] synchronize];
46+
NSString *prev = @"alpha";
47+
NSString *next = @"beta";
48+
[ContextRanking recordCommittedToken:prev];
49+
[ContextRanking recordCommittedToken:next];
50+
NSDictionary *saved = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"ContextRankingBigrams"];
51+
XCTAssertNotNil(saved);
52+
NSDictionary *nexts = saved[prev];
53+
XCTAssertTrue([nexts isKindOfClass:[NSDictionary class]]);
54+
NSNumber *cnt = nexts[next];
55+
XCTAssertTrue(cnt != nil && cnt.integerValue >= 1);
56+
}
57+
4258
@end

0 commit comments

Comments
 (0)