@weiliang0121/capix-transcript 0.0.0-stage → 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wei.liang (https://github.com/weiliang0121)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # @weiliang0121/capix-transcript
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Append-only conversation messages and resource contracts for CAPiX.
4
+
5
+ The public root owns `Message`, `MessageLocator`, `TranscriptRangeLocator`, `TranscriptSnapshot`, and `TranscriptEvent`. Locators identify fixed positions without retaining an entity; snapshots and events describe one source revision, including segment changes. Transport framing and wire acceptance belong to the protocol adapter and are not entity recovery authority.
6
+
7
+ ES modules and TypeScript declarations. Requires Node.js 22.23.2 or newer.
8
+
9
+ ```sh
10
+ npm install @weiliang0121/capix-transcript
11
+ ```
12
+
13
+ ## Public entry points
14
+
15
+ - `@weiliang0121/capix-transcript`
16
+ - `@weiliang0121/capix-transcript/presets`
17
+
18
+ This package is part of CAPiX. The npm distribution contains compiled JavaScript, type declarations, and this package's MIT license. Reference services and client applications are not included in this package or the initial npm release.
19
+
20
+ ## License
21
+
22
+ MIT. Copyright (c) 2026 wei.liang. See [LICENSE](LICENSE).
@@ -0,0 +1,25 @@
1
+ import type { Message } from './types.js';
2
+ export interface TranscriptAppendOperation {
3
+ readonly parentContextId: string;
4
+ readonly childContextId: string;
5
+ readonly position: string;
6
+ }
7
+ export interface TranscriptFixedAppend {
8
+ readonly contract: {
9
+ readonly key: 'transcript.append';
10
+ readonly version: 1;
11
+ };
12
+ readonly operation: TranscriptAppendOperation;
13
+ readonly transcriptId: string;
14
+ readonly messages: readonly Readonly<Message>[];
15
+ }
16
+ export interface TranscriptAppendReceipt {
17
+ readonly transcriptId: string;
18
+ readonly start: number;
19
+ readonly count: number;
20
+ readonly revision: number;
21
+ }
22
+ export interface TranscriptAppendState {
23
+ readonly request: TranscriptFixedAppend;
24
+ readonly receipt: TranscriptAppendReceipt;
25
+ }
package/dist/append.js ADDED
@@ -0,0 +1 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
@@ -0,0 +1,40 @@
1
+ import { FactType } from '@weiliang0121/capix-changes';
2
+ import type { InvocationOrigin } from '@weiliang0121/capix-contracts';
3
+ import type { Message } from './types.js';
4
+ export type TranscriptEvent = Readonly<{
5
+ type: 'transcript';
6
+ transcriptId: string;
7
+ revision: number;
8
+ occurredAt: string;
9
+ origin?: InvocationOrigin;
10
+ change: Readonly<{
11
+ kind: 'messages-appended';
12
+ start: number;
13
+ messages: readonly Readonly<Message>[];
14
+ }> | Readonly<{
15
+ kind: 'segment-started';
16
+ previousStart: number;
17
+ start: number;
18
+ end: number;
19
+ }>;
20
+ }>;
21
+ export declare const TranscriptFacts: FactType<Readonly<{
22
+ type: "transcript";
23
+ transcriptId: string;
24
+ revision: number;
25
+ occurredAt: string;
26
+ origin?: InvocationOrigin;
27
+ change: Readonly<{
28
+ kind: "messages-appended";
29
+ start: number;
30
+ messages: readonly Readonly<Message>[];
31
+ }> | Readonly<{
32
+ kind: "segment-started";
33
+ previousStart: number;
34
+ start: number;
35
+ end: number;
36
+ }>;
37
+ }>>;
38
+ export declare function createTranscriptEvent(transcriptId: string, revision: number, change: TranscriptEvent['change'], origin?: {
39
+ readonly contextId: string;
40
+ }): TranscriptEvent;
package/dist/event.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{FactType}from"@weiliang0121/capix-changes";const TranscriptFacts=new FactType("transcript");function createTranscriptEvent(transcriptId,revision,change,origin){const body=change.kind==="messages-appended"?Object.freeze({...change,messages:Object.freeze([...change.messages])}):Object.freeze({...change});return Object.freeze({type:"transcript",transcriptId,revision,occurredAt:new Date().toISOString(),...origin?{origin:Object.freeze({...origin})}:{},change:body})}__name(createTranscriptEvent,"createTranscriptEvent");export{TranscriptFacts,createTranscriptEvent};
@@ -0,0 +1,9 @@
1
+ export { Transcript, type TranscriptOptions, type TranscriptAppendOptions } from './transcript.js';
2
+ export { MessageRef, TranscriptRangeRef } from './ref.js';
3
+ export type { TranscriptAppendOperation, TranscriptFixedAppend, TranscriptAppendReceipt, TranscriptAppendState } from './append.js';
4
+ export type { TranscriptState, TranscriptCommit, TranscriptPersistence } from './persistence.js';
5
+ export type { TranscriptAppendProgress } from './progress.js';
6
+ export { TranscriptFacts, type TranscriptEvent } from './event.js';
7
+ export { TranscriptResource, TranscriptRangeReader, type TranscriptAppendSource } from './resource.js';
8
+ export { TranscriptViewSource, type TranscriptViewSummary } from './view.js';
9
+ export type { Message, MessageLocator, TranscriptRangeLocator, TranscriptSnapshot } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ import{Transcript}from"./transcript.js";import{MessageRef,TranscriptRangeRef}from"./ref.js";import{TranscriptFacts}from"./event.js";import{TranscriptResource,TranscriptRangeReader}from"./resource.js";import{TranscriptViewSource}from"./view.js";export{MessageRef,Transcript,TranscriptFacts,TranscriptRangeReader,TranscriptRangeRef,TranscriptResource,TranscriptViewSource};
@@ -0,0 +1,25 @@
1
+ import type { Message } from './types.js';
2
+ import type { TranscriptAppendState } from './append.js';
3
+ export interface TranscriptState {
4
+ readonly id: string;
5
+ readonly revision: number;
6
+ readonly starts: readonly number[];
7
+ readonly messages: readonly Readonly<Message>[];
8
+ readonly appends?: readonly TranscriptAppendState[];
9
+ }
10
+ export type TranscriptCommit = {
11
+ readonly transcriptId: string;
12
+ readonly expectedRevision: number;
13
+ readonly revision: number;
14
+ } & ({
15
+ readonly kind: 'append';
16
+ readonly start: number;
17
+ readonly messages: readonly Readonly<Message>[];
18
+ readonly fixed?: TranscriptAppendState;
19
+ } | {
20
+ readonly kind: 'start-segment';
21
+ readonly start: number;
22
+ });
23
+ export interface TranscriptPersistence {
24
+ commit(change: TranscriptCommit): undefined;
25
+ }
@@ -0,0 +1 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
@@ -0,0 +1,7 @@
1
+ import type { Message } from './types.js';
2
+ export interface IndexedMessage {
3
+ readonly index: number;
4
+ readonly message: Readonly<Message>;
5
+ }
6
+ export type AdjacentMessageGroup = readonly [IndexedMessage] | readonly [IndexedMessage, IndexedMessage];
7
+ export declare function groupAdjacentPairs(messages: readonly IndexedMessage[]): readonly AdjacentMessageGroup[];
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});function groupAdjacentPairs(messages){if(!Array.isArray(messages))throw new TypeError("Message grouping requires indexed messages");let previous=-1;const entries=Array.from(messages,entry=>{const index=entry?.index,message=entry?.message;if(!Number.isSafeInteger(index)||index<=previous)throw new RangeError("Message grouping requires strictly increasing nonnegative indexes");if(message?.role!=="user"&&message?.role!=="assistant")throw new TypeError("Message grouping requires user or assistant messages");return previous=index,Object.freeze({index,message})}),groups=[];for(let position=0;position<entries.length;position++){const current=entries[position],next=entries[position+1];current.message.role==="user"&&next?.message.role==="assistant"&&next.index===current.index+1?(groups.push(Object.freeze([current,next])),position++):groups.push(Object.freeze([current]))}return Object.freeze(groups)}__name(groupAdjacentPairs,"groupAdjacentPairs");export{groupAdjacentPairs};
@@ -0,0 +1,3 @@
1
+ import type { TranscriptRangeLocator } from './types.js';
2
+ export interface TranscriptAppendProgress extends TranscriptRangeLocator {
3
+ }
@@ -0,0 +1 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
package/dist/ref.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { Transcript } from './transcript.js';
2
+ export declare class MessageRef {
3
+ readonly transcript: Transcript;
4
+ readonly index: number;
5
+ constructor(transcript: Transcript, index: number);
6
+ read(): Readonly<import("./types.js").Message>;
7
+ }
8
+ export declare class TranscriptRangeRef {
9
+ readonly transcript: Transcript;
10
+ readonly start: number;
11
+ readonly end: number;
12
+ constructor(transcript: Transcript, start: number, end: number);
13
+ get length(): number;
14
+ ref(index: number): MessageRef;
15
+ read(index: number): Readonly<import("./types.js").Message>;
16
+ refs(): readonly MessageRef[];
17
+ }
package/dist/ref.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{isSafeInteger}from"es-toolkit/compat";class MessageRef{static{__name(this,"MessageRef")}transcript;index;constructor(transcript,index){if(this.transcript=transcript,this.index=index,!isSafeInteger(index)||index<0||!transcript.get(index))throw new RangeError("Message index does not exist");Object.freeze(this)}read(){return this.transcript.get(this.index)}}class TranscriptRangeRef{static{__name(this,"TranscriptRangeRef")}transcript;start;end;constructor(transcript,start,end){if(this.transcript=transcript,this.start=start,this.end=end,!isSafeInteger(start)||!isSafeInteger(end)||start<0||end<start||end>transcript.length)throw new RangeError("Transcript range does not exist");Object.freeze(this)}get length(){return this.end-this.start}ref(index){if(index<this.start||index>=this.end)throw new RangeError("Message is outside the fixed history range");return new MessageRef(this.transcript,index)}read(index){if(!isSafeInteger(index)||index<this.start||index>=this.end)throw new RangeError("Message is outside the fixed history range");return this.transcript.get(index)}refs(){return Array.from({length:this.length},(_,offset)=>this.ref(this.start+offset))}}export{MessageRef,TranscriptRangeRef};
@@ -0,0 +1,48 @@
1
+ import { Transcript } from './transcript.js';
2
+ import type { Message, TranscriptRangeLocator } from './types.js';
3
+ import { ResourceOffer } from '@weiliang0121/capix-resources';
4
+ import type { ResourceEndpointActivity } from '@weiliang0121/capix-resources';
5
+ import { TranscriptRangeRef } from './ref.js';
6
+ import { type ProgressReceiver } from '@weiliang0121/capix-changes';
7
+ import type { TranscriptAppendProgress } from './progress.js';
8
+ export interface TranscriptAppendSource {
9
+ readonly transcript: Transcript;
10
+ readonly progress?: ProgressReceiver<TranscriptAppendProgress>;
11
+ }
12
+ export declare class TranscriptRangeReader {
13
+ #private;
14
+ readonly start: number;
15
+ readonly end: number;
16
+ readonly transcriptId: string;
17
+ readonly revision: number;
18
+ constructor(range: TranscriptRangeRef, activity?: ResourceEndpointActivity, revision?: number);
19
+ get length(): number;
20
+ locate(): TranscriptRangeLocator;
21
+ read(index: number): Readonly<Message>;
22
+ slice(start: number, end?: number): TranscriptRangeReader;
23
+ entries(): readonly Readonly<{
24
+ index: number;
25
+ message: Readonly<Message>;
26
+ }>[];
27
+ }
28
+ export declare const TranscriptResource: Readonly<{
29
+ append: ResourceOffer<Transcript | TranscriptAppendSource, Readonly<{
30
+ applyFixed(input: {
31
+ readonly position: string;
32
+ readonly messages: readonly Message[];
33
+ }): unknown;
34
+ append(messages: readonly Message[]): readonly Readonly<{
35
+ transcriptId: string;
36
+ index: number;
37
+ message: Readonly<Message>;
38
+ }>[];
39
+ }>, Transcript | TranscriptAppendSource, never>;
40
+ segmentation: ResourceOffer<Transcript, Readonly<{
41
+ current: () => TranscriptRangeReader;
42
+ start: (index: number) => void;
43
+ }>, Transcript, never>;
44
+ fixedRange: ResourceOffer<TranscriptRangeRef, TranscriptRangeReader, {
45
+ range: TranscriptRangeRef;
46
+ revision: number;
47
+ }, never>;
48
+ }>;
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{TranscriptFacts}from"./event.js";import{Transcript}from"./transcript.js";import{ResourceOffer}from"@weiliang0121/capix-resources";import{TranscriptRangeRef}from"./ref.js";import{ProgressChannel}from"@weiliang0121/capix-changes";class TranscriptRangeReader{static{__name(this,"TranscriptRangeReader")}#range;#activity;start;end;transcriptId;revision;constructor(range,activity,revision=range.transcript.revision){this.#range=range,this.#activity=activity,this.start=range.start,this.end=range.end,this.transcriptId=range.transcript.id,this.revision=revision,Object.freeze(this)}get length(){return this.end-this.start}locate(){return Object.freeze({transcriptId:this.transcriptId,start:this.start,end:this.end,revision:this.revision})}read(index){const read=__name(()=>this.#range.read(index),"read");return this.#activity?this.#activity.runSync("read",{input:__name(()=>({transcriptId:this.transcriptId,index}),"input"),output:__name(()=>({transcriptId:this.transcriptId,index}),"output")},read):read()}slice(start,end=this.end){if(this.#activity?.validate(),start<this.start||end>this.end)throw new RangeError("Requested range exceeds available messages");return new TranscriptRangeReader(new TranscriptRangeRef(this.#range.transcript,start,end),this.#activity,this.revision)}entries(){const read=__name(()=>Object.freeze(Array.from({length:this.length},(_,offset)=>{const index=this.start+offset;return Object.freeze({index,message:this.#range.read(index)})})),"read");return this.#activity?this.#activity.runSync("entries",{input:__name(()=>this.locate(),"input"),output:__name(()=>this.locate(),"output")},read):read()}}const TranscriptResource=Object.freeze({append:new ResourceOffer({name:"transcript.append",expose:__name((source,activity)=>{const transcript=source instanceof Transcript?source:source.transcript,progress=source instanceof Transcript||!source.progress?void 0:new ProgressChannel(source.progress);return Object.freeze({applyFixed(input){return activity.runSync("applyFixed",{input:__name(()=>({position:input.position,count:input.messages.length}),"input"),output:__name(receipt=>receipt,"output")},operation=>transcript.applyFixed({contract:{key:"transcript.append",version:1},operation:{...activity.reception(),position:input.position},transcriptId:transcript.id,messages:input.messages},{origin:operation.origin,observation:operation.facts(TranscriptFacts),progress}))},append(messages){return activity.runSync("append",{input:__name(()=>({count:messages.length}),"input"),output:__name(result=>({transcriptId:transcript.id,indexes:result.map(item=>item.index),revision:transcript.revision}),"output")},operation=>{const start=transcript.length;return transcript.appendFrom(messages,{origin:operation.origin,observation:operation.facts(TranscriptFacts),progress}),Object.freeze(messages.map((_,offset)=>Object.freeze({transcriptId:transcript.id,index:start+offset,message:transcript.get(start+offset)})))})}})},"expose"),timing:"access",describe:__name(source=>({transcriptId:(source instanceof Transcript?source:source.transcript).id}),"describe")}),segmentation:new ResourceOffer({name:"transcript.segmentation",expose:__name((transcript,activity)=>Object.freeze({current:__name(()=>activity.runSync("current",{output:__name(reader=>reader.locate(),"output")},()=>new TranscriptRangeReader(new TranscriptRangeRef(transcript,transcript.activeStart,transcript.length),activity)),"current"),start:__name(index=>activity.runSync("start",{input:__name(()=>({start:index}),"input"),output:__name(()=>({transcriptId:transcript.id,start:transcript.activeStart,end:transcript.length,revision:transcript.revision}),"output")},operation=>transcript.startSegment(index,{origin:operation.origin,observation:operation.facts(TranscriptFacts)})),"start")}),"expose"),timing:"access",describe:__name(transcript=>({transcriptId:transcript.id}),"describe")}),fixedRange:new ResourceOffer({name:"transcript.fixedRange",expose:__name((captured,activity)=>new TranscriptRangeReader(captured.range,activity,captured.revision),"expose"),timing:"binding",capture:__name(range=>({range,revision:range.transcript.revision}),"capture"),describe:__name(({range,revision})=>({transcriptId:range.transcript.id,start:range.start,end:range.end,revision}),"describe")})});export{TranscriptRangeReader,TranscriptResource};
@@ -0,0 +1,47 @@
1
+ import { type ChangeProvenance } from '@weiliang0121/capix-changes';
2
+ import type { EventConsumer } from '@weiliang0121/capix-changes';
3
+ import { MessageRef, TranscriptRangeRef } from './ref.js';
4
+ import type { Message } from './types.js';
5
+ import type { ChangeCapture } from '@weiliang0121/capix-changes';
6
+ import { type TranscriptEvent } from './event.js';
7
+ import type { ProgressChannel } from '@weiliang0121/capix-changes';
8
+ import type { TranscriptAppendProgress } from './progress.js';
9
+ import type { TranscriptPersistence, TranscriptState } from './persistence.js';
10
+ import type { TranscriptAppendReceipt, TranscriptFixedAppend } from './append.js';
11
+ export interface TranscriptOptions {
12
+ observation?: EventConsumer<TranscriptEvent>;
13
+ changes?: ChangeCapture<TranscriptEvent>;
14
+ persistence?: TranscriptPersistence;
15
+ }
16
+ export interface TranscriptAppendOptions extends ChangeProvenance<TranscriptEvent> {
17
+ readonly progress?: ProgressChannel<TranscriptAppendProgress>;
18
+ }
19
+ export declare class Transcript {
20
+ #private;
21
+ constructor(messages?: readonly Message[], options?: TranscriptOptions);
22
+ get id(): string;
23
+ static restore(state: TranscriptState, options?: TranscriptOptions): Transcript;
24
+ exportState(): TranscriptState;
25
+ get length(): number;
26
+ get revision(): number;
27
+ get activeStart(): number;
28
+ segments(): readonly TranscriptRangeRef[];
29
+ startSegment(start: number, provenance?: ChangeProvenance<TranscriptEvent>): void;
30
+ append(...messages: readonly Message[]): void;
31
+ appendFrom(messages: readonly Message[], provenance?: TranscriptAppendOptions): void;
32
+ confirmFixed(value: TranscriptFixedAppend): TranscriptAppendReceipt | undefined;
33
+ applyFixed(value: TranscriptFixedAppend, provenance?: TranscriptAppendOptions): TranscriptAppendReceipt;
34
+ private appendAccepted;
35
+ private acceptFixed;
36
+ private confirmAccepted;
37
+ private matchesMessages;
38
+ private capture;
39
+ private enterMutation;
40
+ private nextRevision;
41
+ private commit;
42
+ current(role: Message['role']): Readonly<Message> | undefined;
43
+ get(index: number): Readonly<Message> | undefined;
44
+ ref(index: number): MessageRef;
45
+ snapshot(end?: number): readonly Readonly<Message>[];
46
+ private static accept;
47
+ }
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{publishFact}from"@weiliang0121/capix-changes";import{randomUUID}from"node:crypto";import{isArray,isSafeInteger}from"es-toolkit/compat";import{isNonBlankString}from"./validation.js";import{isObject}from"./validation.js";import{MessageRef,TranscriptRangeRef}from"./ref.js";import{createTranscriptEvent}from"./event.js";import{RuntimeError}from"@weiliang0121/capix-contracts";import{PersistenceError}from"@weiliang0121/capix-diagnostics";class Transcript{static{__name(this,"Transcript")}#id=randomUUID();#observation;#mutating=!1;#changes;#persistence;#revision=0;#messages=[];#starts=[0];#appends=new Map;constructor(messages=[],options={}){if(this.#observation=options.observation,this.#changes=options.changes,this.#persistence=options.persistence,!isArray(messages))throw new TypeError("\u6D88\u606F\u96C6\u5408\u5FC5\u987B\u662F\u6570\u7EC4\u3002");this.append(...messages)}get id(){return this.#id}static restore(state,options={}){if(!isObject(state)||!isNonBlankString(state.id)||!isArray(state.messages)||!isArray(state.starts))throw new TypeError("Invalid persisted transcript");const messages=Array.from(state.messages,message=>Transcript.accept(message)),starts=Array.from(state.starts);if(!starts.length||starts[0]!==0||starts.some((start,index)=>!isSafeInteger(start)||start<0||index>0&&(start<=starts[index-1]||start>=messages.length)))throw new TypeError("Invalid persisted transcript segments");const minimumRevision=messages.length?starts.length:0,maximumRevision=messages.length+starts.length-1;if(!isSafeInteger(state.revision)||state.revision<minimumRevision||state.revision>maximumRevision)throw new TypeError("Invalid persisted transcript revision");const transcript=new Transcript([],options);transcript.#id=state.id,transcript.#revision=state.revision,transcript.#starts.length=0;for(const start of starts)transcript.#starts.push(start);for(const message of messages)transcript.#messages.push(message);if(state.appends!==void 0){if(!isArray(state.appends))throw new TypeError("Invalid persisted transcript append operations");let previousEnd=0,previousRevision=0;for(const saved of state.appends){if(!isObject(saved)||!isObject(saved.receipt))throw new TypeError("Invalid persisted transcript append receipt");const request=transcript.acceptFixed(saved.request),value=saved.receipt;if(value.transcriptId!==transcript.id||!isSafeInteger(value.start)||value.start<previousEnd||!isSafeInteger(value.count)||value.count!==request.messages.length||value.start+value.count>messages.length||!isSafeInteger(value.revision)||value.revision<=previousRevision||value.revision>state.revision)throw new TypeError("Invalid persisted transcript append receipt");if(!transcript.matchesMessages(value.start,request.messages))throw new TypeError("Persisted transcript append does not match messages");const key=operationKey(request.operation);if(transcript.#appends.has(key))throw new TypeError("Duplicate persisted transcript append operation");const receipt=Object.freeze({transcriptId:transcript.id,start:value.start,count:value.count,revision:value.revision});transcript.#appends.set(key,Object.freeze({request,receipt})),previousEnd=value.start+value.count,previousRevision=value.revision}}return transcript}exportState(){return Object.freeze({id:this.id,revision:this.#revision,starts:Object.freeze([...this.#starts]),messages:Object.freeze([...this.#messages]),...this.#appends.size?{appends:Object.freeze([...this.#appends.values()])}:{}})}get length(){return this.#messages.length}get revision(){return this.#revision}get activeStart(){return this.#starts.at(-1)}segments(){return this.#starts.map((start,index)=>new TranscriptRangeRef(this,start,this.#starts[index+1]??this.length))}startSegment(start,provenance={}){this.enterMutation();try{if(!isSafeInteger(start)||start<=this.activeStart||start>=this.length)throw new RangeError("New segment must start inside the active range");const previousStart=this.activeStart,end=this.length,event=createTranscriptEvent(this.id,this.nextRevision(),{kind:"segment-started",previousStart,start,end},provenance.origin);this.commit(Object.freeze({kind:"start-segment",transcriptId:this.id,expectedRevision:this.#revision,revision:event.revision,start})),this.#starts.push(start),this.#revision=event.revision,this.capture(event,provenance.observation)}finally{this.#mutating=!1}}append(...messages){this.appendFrom(messages)}appendFrom(messages,provenance={}){this.enterMutation();try{if(!isArray(messages))throw new TypeError("\u6D88\u606F\u96C6\u5408\u5FC5\u987B\u662F\u6570\u7EC4\u3002");const accepted=Object.freeze(Array.from(messages,message=>Transcript.accept(message)));if(!accepted.length)return;this.appendAccepted(accepted,provenance)}finally{this.#mutating=!1}}confirmFixed(value){const request=this.acceptFixed(value);return this.confirmAccepted(request)}applyFixed(value,provenance={}){this.enterMutation();try{const request=this.acceptFixed(value);return this.confirmAccepted(request)??this.appendAccepted(request.messages,provenance,request)}finally{this.#mutating=!1}}appendAccepted(messages,provenance,request){const start=this.length,event=createTranscriptEvent(this.id,this.nextRevision(),{kind:"messages-appended",start,messages},provenance.origin),receipt=Object.freeze({transcriptId:this.id,start,count:messages.length,revision:event.revision}),fixed=request?Object.freeze({request,receipt}):void 0;this.commit(Object.freeze({kind:"append",transcriptId:this.id,expectedRevision:this.#revision,revision:event.revision,start,messages,...fixed?{fixed}:{}}));for(const message of messages)this.#messages.push(message);return this.#revision=event.revision,fixed&&this.#appends.set(operationKey(fixed.request.operation),fixed),this.capture(event,provenance.observation,provenance.progress),receipt}acceptFixed(value){if(!isObject(value)||!isObject(value.operation)||!isNonBlankString(value.operation.parentContextId)||!isNonBlankString(value.operation.childContextId)||!isNonBlankString(value.operation.position)||!isObject(value.contract)||!isArray(value.messages)||!value.messages.length)throw new TypeError("Invalid fixed transcript append request");if(value.transcriptId!==this.id||value.contract.key!=="transcript.append"||value.contract.version!==1)throw appendConflict();const operation=Object.freeze({parentContextId:value.operation.parentContextId,childContextId:value.operation.childContextId,position:value.operation.position}),messages=Object.freeze(Array.from(value.messages,message=>Transcript.accept(message)));return Object.freeze({contract:Object.freeze({key:"transcript.append",version:1}),operation,transcriptId:this.id,messages})}confirmAccepted(request){const saved=this.#appends.get(operationKey(request.operation));if(saved){if(saved.request.messages.length!==request.messages.length||!this.matchesMessages(saved.receipt.start,request.messages))throw appendConflict();return saved.receipt}}matchesMessages(start,messages){return messages.every((message,offset)=>this.#messages[start+offset]?.role===message.role&&this.#messages[start+offset]?.content===message.content)}capture(event,observation=this.#observation,progress){event.change.kind==="messages-appended"&&progress?.publish(Object.freeze({transcriptId:this.id,start:event.change.start,end:event.change.start+event.change.messages.length,revision:event.revision})),publishFact(event,this.#changes,observation)}enterMutation(){if(this.#mutating)throw new Error("Transcript persistence or change publication cannot reenter mutation");this.#mutating=!0}nextRevision(){if(!isSafeInteger(this.#revision+1))throw new RangeError("Transcript revision is exhausted");return this.#revision+1}commit(change){try{const result=this.#persistence?.commit(change);if(result!==void 0)throw Promise.resolve(result).catch(()=>{}),new TypeError("Transcript persistence must complete synchronously and return undefined")}catch(cause){throw cause instanceof PersistenceError?cause:new PersistenceError("transcript.commit",cause)}}current(role){return this.#messages.findLast(message2=>message2.role===role)}get(index){return this.#messages[index]}ref(index){return new MessageRef(this,index)}snapshot(end=this.length){if(!isSafeInteger(end)||end<0||end>this.length)throw new RangeError("Transcript boundary does not exist");return this.#messages.slice(0,end)}static accept(value){if(!isObject(value))throw new TypeError("\u6D88\u606F\u5FC5\u987B\u662F\u5305\u542B role \u548C content \u7684\u5BF9\u8C61\u3002");const message=value;if(message.role!=="user"&&message.role!=="assistant")throw new TypeError("\u6D88\u606F\u89D2\u8272\u5FC5\u987B\u662F user \u6216 assistant\u3002");if(!isNonBlankString(message.content))throw new TypeError("\u6D88\u606F\u5185\u5BB9\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\u3002");return Object.freeze({role:message.role,content:message.content})}}function operationKey(operation){return JSON.stringify([operation.parentContextId,operation.childContextId,operation.position])}__name(operationKey,"operationKey");function appendConflict(){return new RuntimeError("TRANSCRIPT_APPEND_CONFLICT","\u56FA\u5B9A\u6D88\u606F\u8FFD\u52A0\u7684\u76EE\u6807\u3001\u5951\u7EA6\u6216\u5185\u5BB9\u4E0E\u539F\u64CD\u4F5C\u4E0D\u4E00\u81F4\u3002")}__name(appendConflict,"appendConflict");export{Transcript};
@@ -0,0 +1,22 @@
1
+ export interface Message {
2
+ role: 'user' | 'assistant';
3
+ content: string;
4
+ }
5
+ export interface MessageLocator {
6
+ readonly transcriptId: string;
7
+ readonly index: number;
8
+ }
9
+ export interface TranscriptRangeLocator {
10
+ readonly transcriptId: string;
11
+ readonly start: number;
12
+ readonly end: number;
13
+ readonly revision?: number;
14
+ }
15
+ export interface TranscriptSnapshot {
16
+ readonly id: string;
17
+ readonly revision: number;
18
+ readonly start: 0;
19
+ readonly end: number;
20
+ readonly starts: readonly number[];
21
+ readonly messages: readonly Readonly<Message>[];
22
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
@@ -0,0 +1,2 @@
1
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
2
+ export declare function isNonBlankString(value: unknown): value is string;
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{isArray,isObjectLike}from"es-toolkit/compat";import{isString}from"es-toolkit/predicate";function isObject(value){return isObjectLike(value)&&!isArray(value)}__name(isObject,"isObject");function isNonBlankString(value){return isString(value)&&/\S/.test(value)}__name(isNonBlankString,"isNonBlankString");export{isNonBlankString,isObject};
package/dist/view.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { Message } from './types.js';
2
+ import type { SyncViewSource } from '@weiliang0121/capix-view';
3
+ import type { Transcript } from './transcript.js';
4
+ export interface TranscriptViewSummary {
5
+ readonly role: Message['role'];
6
+ }
7
+ type TranscriptViewContract = SyncViewSource<number, TranscriptViewSummary, Readonly<Message>>;
8
+ type SourceStamp = Parameters<TranscriptViewContract['read']>[1];
9
+ export declare class TranscriptViewSource implements TranscriptViewContract {
10
+ #private;
11
+ readonly id: string;
12
+ readonly scope: string;
13
+ constructor(transcript: Transcript, keys?: readonly number[]);
14
+ index(): ReturnType<TranscriptViewContract['index']>;
15
+ inspect(keys: readonly number[]): ReturnType<TranscriptViewContract['inspect']>;
16
+ read(keys: readonly number[], expected: SourceStamp): ReturnType<TranscriptViewContract['read']>;
17
+ check(expected: SourceStamp): boolean;
18
+ private stamp;
19
+ private requireCurrent;
20
+ private acceptKeys;
21
+ private requireMessage;
22
+ }
23
+ export {};
package/dist/view.js ADDED
@@ -0,0 +1,2 @@
1
+ /*! MIT License | Copyright (c) 2026 wei.liang | See LICENSE */
2
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import{RuntimeError}from"@weiliang0121/capix-contracts";class TranscriptViewSource{static{__name(this,"TranscriptViewSource")}id;scope;#transcript;#keys;#allowed;constructor(transcript,keys){if(this.#transcript=transcript,this.id=transcript.id,keys!==void 0){this.#keys=this.acceptKeys(keys);for(const key of this.#keys)this.requireMessage(key);this.#allowed=new Set(this.#keys)}this.scope=this.#keys===void 0?"whole":`keys:${JSON.stringify(this.#keys)}`,Object.freeze(this)}index(){const stamp=this.stamp(),keys=this.#keys??Array.from({length:this.#transcript.length},(_,index)=>index),entries=Object.freeze(keys.map(key=>Object.freeze({key,summary:Object.freeze({role:this.requireMessage(key).role})})));return this.requireCurrent(stamp),Object.freeze({stamp,entries})}inspect(keys){const stamp=this.stamp(),selected=this.acceptKeys(keys),entries=Object.freeze(selected.map(key=>{if(this.#allowed&&!this.#allowed.has(key))throw new RangeError("Message is outside the view source scope");return Object.freeze({key,summary:Object.freeze({role:this.requireMessage(key).role})})}));return this.requireCurrent(stamp),Object.freeze({stamp,entries})}read(keys,expected){const basis=Object.freeze({id:expected.id,scope:expected.scope,version:expected.version});this.requireCurrent(basis);const selected=this.acceptKeys(keys),entries=Object.freeze(selected.map(key=>{if(this.#allowed&&!this.#allowed.has(key))throw new RangeError("Message is outside the view source scope");return Object.freeze({key,value:this.requireMessage(key)})}));return this.requireCurrent(basis),Object.freeze({stamp:this.stamp(),entries})}check(expected){return expected?.id===this.id&&expected.scope===this.scope&&expected.version===String(this.#transcript.revision)}stamp(){return Object.freeze({id:this.id,scope:this.scope,version:String(this.#transcript.revision)})}requireCurrent(expected){if(!this.check(expected))throw new RuntimeError("VIEW_SOURCE_CONFLICT","Transcript view source has changed")}acceptKeys(keys){if(!Array.isArray(keys))throw new TypeError("Transcript view keys must be an array");const accepted=Array.from(keys);if(accepted.some(key=>!Number.isSafeInteger(key)||key<0)||new Set(accepted).size!==accepted.length)throw new TypeError("Transcript view requires unique nonnegative integer keys");return Object.freeze(accepted)}requireMessage(key){const message=this.#transcript.get(key);if(!message)throw new RangeError("Transcript view message does not exist");return message}}export{TranscriptViewSource};
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@weiliang0121/capix-transcript",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./presets": {
12
+ "types": "./dist/presets.d.ts",
13
+ "import": "./dist/presets.js"
14
+ }
15
+ },
16
+ "dependencies": {
17
+ "es-toolkit": "1.52.0",
18
+ "@weiliang0121/capix-contracts": "0.2.0",
19
+ "@weiliang0121/capix-diagnostics": "0.2.0",
20
+ "@weiliang0121/capix-resources": "0.2.0",
21
+ "@weiliang0121/capix-changes": "0.2.0",
22
+ "@weiliang0121/capix-view": "0.2.0"
23
+ },
24
+ "description": "Append-only conversation messages and resource contracts for CAPiX.",
25
+ "license": "MIT",
26
+ "author": "wei.liang (https://github.com/weiliang0121)",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/weiliang0121/capix.git",
30
+ "directory": "packages/transcript"
31
+ },
32
+ "engines": {
33
+ "node": ">=22.23.2"
34
+ },
35
+ "main": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "files": [
38
+ "dist/**/*.js",
39
+ "dist/**/*.d.ts",
40
+ "LICENSE",
41
+ "README.md"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "registry": "https://registry.npmjs.org"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.json",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit",
50
+ "test": "node ../../scripts/test/run.mjs --package ."
51
+ }
6
52
  }