You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[1b] Route getDraft and saveDraft through an async repository #431
Async repository interface and in-memory implementation over today's format; getDraft/saveDraft route through it; a Node seeding path through it replaces the #407 harness's PAPI seeding.
Done when: the existing unit and e2e suites pass unchanged, a newer build's draft survives an attempted write, and the #407 harness seeds every tier without PAPI.
A proposal for 1b's interface, to make it easy to also implement in C# for the Harmony store (#462). It doesn't change the plan; it fills in what the plan leaves to 1b. I worked through it with Claude, comparing it against how FW Lite's MiniLcm and Harmony split the work, so take it as a reasonable starting point to poke holes in, not a decision.
Pieces of work stay in the host commands (writeGloss, createPhrase, ..., 3d to 3i), each with its own request type. The repository receives their result: a ChangeSet of the records that changed, applied atomically as one commit, one undo step and one change event. Its intent field only names the command.
ChangeSet could be explicit: one typed list per kind instead of a polymorphic record list. Same shape as today's TextAnalysis; a plain class in C#.
Reads named and typed per kind (getTokenAnalyses(ids), findLinksByToken(tokenRefs), ...), rather than one generic get/find.
Only JSON-plain values across the seam: no Map, Set, callbacks or class instances; remote changes arrive as a PAPI event.
Since the plan keeps the interlinear rules in the host, the store doesn't need MiniLcm-style per-intent changes. The exception is a case that merges badly as a record diff between teammates (likely the morpheme list or a phrase's member tokens), which could get its own Harmony change type.
Interface sketch (TypeScript, not compiled)
typeRecordId=string;// uuidtypeBookCode=string;// 'GEN'typeTokenRef=string;// 'GEN 1:1:5'typeSegmentId=string;// 'GEN 1:1'// Record types are today's, from interlinearizer.d.ts; token and segment links get ids in 1a.interfaceScopeRecord{id: RecordId;modelVersion: number;name?: string;description?: string;analysisLanguages: string[];segmentation?: SegmentationDelta;sourceProjectId: string;targetProjectId?: string;createdAt: string;updatedAt: string;}interfaceChanges<T>{puts: T[];deletes: RecordId[]}/** One command's result, applied all-or-nothing. */interfaceChangeSet{id: string;// a retry with the same id applies onceintent: string;// the command's namescope?: ScopeRecord;tokenAnalyses?: Changes<TokenAnalysis>;tokenLinks?: Changes<TokenAnalysisLink>;phraseAnalyses?: Changes<PhraseAnalysis>;phraseLinks?: Changes<PhraseAnalysisLink>;segmentAnalyses?: Changes<SegmentAnalysis>;segmentLinks?: Changes<SegmentAnalysisLink>;}interfaceApplyResult{inverse: ChangeSet}// undointerfaceLinks{tokenLinks: TokenAnalysisLink[];phraseLinks: PhraseAnalysisLink[];segmentLinks: SegmentAnalysisLink[];}interfaceCatalogQuery{kind: 'tokenAnalysis'|'phraseAnalysis';text?: string;// matches form or any glossbooks?: BookCode[];usage: 'any'|'used'|'unused';sortField: 'form'|'gloss'|'usage'|'updatedAt';sortDirection: 'asc'|'desc';offset: number;count: number;}interfaceCatalogRow{tokenAnalysis?: TokenAnalysis;// exactly one of these two is setphraseAnalysis?: PhraseAnalysis;usageCount: number;books: BookCode[];}interfaceCapabilities{drafts: boolean;// local: true; shared: falsereadOnly: boolean;// PT9 imports, Observersremote: boolean;// has teammates' changes to pull}// Reads promise no order; a command that must pick one record picks by rule (1a).interfaceAnalysisRepository{readonlycapabilities: Capabilities;getScope(): Promise<ScopeRecord>;getTokenAnalyses(ids: RecordId[]): Promise<TokenAnalysis[]>;// missing ids are omittedgetPhraseAnalyses(ids: RecordId[]): Promise<PhraseAnalysis[]>;getSegmentAnalyses(ids: RecordId[]): Promise<SegmentAnalysis[]>;/** Token links on these tokens, plus phrase links touching any of them. */findLinksByToken(tokenRefs: TokenRef[]): Promise<Pick<Links,'tokenLinks'|'phraseLinks'>>;findLinksBySegment(segmentIds: SegmentId[]): Promise<SegmentAnalysisLink[]>;findLinksToAnalyses(analysisIds: RecordId[]): Promise<Links>;findLinksInBook(book: BookCode,statuses?: AssignmentStatus[]): Promise<Links>;findTokenAnalysesByForm(normalizedForms: string[]): Promise<TokenAnalysis[]>;findPhraseAnalysesByForm(normalizedForms: string[]): Promise<PhraseAnalysis[]>;countUsages(analysisIds: RecordId[]): Promise<Record<RecordId,number>>;getCatalogPage(query: CatalogQuery): Promise<{rows: CatalogRow[];total: number}>;/** Refused when read-only or when a record carries a newer model version. */apply(changes: ChangeSet): Promise<ApplyResult>;/** Teammates' changes since the last pull, as one ChangeSet. */pullRemoteChanges(): Promise<ChangeSet>;/** Temporary until 4c. */exportAll(): Promise<TextAnalysis>;replaceAll(analysis: TextAnalysis,intent: string): Promise<ApplyResult>;close(): Promise<void>;}// Remote changes ready to pull: PAPI network event, payload { scopeId }.
The same in C# (excerpt)
publicrecordChanges<T>(IReadOnlyList<T>Puts,IReadOnlyList<Guid>Deletes);publicrecordChangeSet(stringId,stringIntent,ScopeRecord?Scope,Changes<TokenAnalysis>?TokenAnalyses,Changes<TokenAnalysisLink>?TokenLinks,Changes<PhraseAnalysis>?PhraseAnalyses,Changes<PhraseAnalysisLink>?PhraseLinks,Changes<SegmentAnalysis>?SegmentAnalyses,Changes<SegmentAnalysisLink>?SegmentLinks);publicinterfaceIAnalysisStore{Task<IReadOnlyList<TokenAnalysis>>GetTokenAnalyses(IReadOnlyList<Guid>ids);Task<Links>FindLinksToAnalyses(IReadOnlyList<Guid>analysisIds);Task<Dictionary<Guid,int>>CountUsages(IReadOnlyList<Guid>analysisIds);Task<ApplyResult>Apply(ChangeSetchanges);// ...the rest mirror the TypeScript one-to-one}// Serialize with camelCase names, string enums and JsonIgnoreCondition.WhenWritingNull.
Activity
myieye commentedon Oct 16, 2026
A proposal for 1b's interface, to make it easy to also implement in C# for the Harmony store (#462). It doesn't change the plan; it fills in what the plan leaves to 1b. I worked through it with Claude, comparing it against how FW Lite's MiniLcm and Harmony split the work, so take it as a reasonable starting point to poke holes in, not a decision.
writeGloss,createPhrase, ..., 3d to 3i), each with its own request type. The repository receives their result: aChangeSetof the records that changed, applied atomically as one commit, one undo step and one change event. Itsintentfield only names the command.ChangeSetcould be explicit: one typed list per kind instead of a polymorphic record list. Same shape as today'sTextAnalysis; a plain class in C#.getTokenAnalyses(ids),findLinksByToken(tokenRefs), ...), rather than one genericget/find.Map,Set, callbacks or class instances; remote changes arrive as a PAPI event.null, with 1c ([1c] Add a repository conformance suite #432) checking it.Since the plan keeps the interlinear rules in the host, the store doesn't need MiniLcm-style per-intent changes. The exception is a case that merges badly as a record diff between teammates (likely the morpheme list or a phrase's member tokens), which could get its own Harmony change type.
Interface sketch (TypeScript, not compiled)
The same in C# (excerpt)
imnasnainaec commentedon Oct 16, 2026
Route getDraft and saveDraft through an async repository (#431)