chore: push all design docs, V2 plan specs, and current working state
Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2, AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code changes across packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,11 @@
|
||||
* Implements: ingest(durable) → EventStore.append, ingest_ephemeral → EventBus.publish
|
||||
* Per system-detailed-design.md §5.1 and runtime-semantics-v1.md §2.
|
||||
*
|
||||
* Construction-time binding (round5 Wf-A A.3/A.4):
|
||||
* - `event_store` is supplied at construction. The module-level `eventStore`
|
||||
* and `eventIngestor` singletons are removed; SessionManager constructs a
|
||||
* per-session pair and hands them to RuntimeApp.
|
||||
*
|
||||
* Rules:
|
||||
* - Never creates scheduler tasks, permission decisions, or memory promotions itself
|
||||
* - Those are follow-up events emitted by owning services
|
||||
@@ -14,17 +19,7 @@
|
||||
import type { RuntimeEvent, EventFilter } from '@aircoding/contracts'
|
||||
import { eventSchemaRegistry, type EventPersistence } from './EventSchemaRegistry.js'
|
||||
import { eventBus, type EventBus } from './EventBus.js'
|
||||
|
||||
// Import EventStore lazily to avoid circular dependency
|
||||
let _eventStore: any = null
|
||||
async function getEventStore() {
|
||||
if (!_eventStore) {
|
||||
// Use dynamic import for ESM
|
||||
const mod = await import('./EventStore.js')
|
||||
_eventStore = mod.eventStore
|
||||
}
|
||||
return _eventStore
|
||||
}
|
||||
import type { EventStore } from './EventStore.js'
|
||||
|
||||
// =============================================================================
|
||||
// Interfaces (for backward compatibility with existing code)
|
||||
@@ -98,11 +93,14 @@ export function createNullEventIngestor(): IEventIngestor {
|
||||
*/
|
||||
export class EventIngestorImpl implements IEventIngestor {
|
||||
private bus: EventBus
|
||||
private event_store: EventStore
|
||||
|
||||
constructor(options?: {
|
||||
constructor(options: {
|
||||
bus?: EventBus
|
||||
event_store: EventStore
|
||||
}) {
|
||||
this.bus = options?.bus ?? eventBus
|
||||
this.bus = options.bus ?? eventBus
|
||||
this.event_store = options.event_store
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,9 +119,8 @@ export class EventIngestorImpl implements IEventIngestor {
|
||||
)
|
||||
}
|
||||
|
||||
// Delegate to EventStore (which handles tx + projection + post-commit publish)
|
||||
const store = await getEventStore()
|
||||
await store.append(event)
|
||||
// Delegate to bound EventStore (which handles tx + projection + post-commit publish)
|
||||
await this.event_store.append(event)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,8 +160,7 @@ export class EventIngestorImpl implements IEventIngestor {
|
||||
}
|
||||
|
||||
if (policy === 'durable') {
|
||||
const store = await getEventStore()
|
||||
await store.append_many(events as RuntimeEvent<unknown>[])
|
||||
await this.event_store.append_many(events as RuntimeEvent<unknown>[])
|
||||
} else {
|
||||
for (const event of events) {
|
||||
this.bus.publish(event)
|
||||
@@ -176,8 +172,7 @@ export class EventIngestorImpl implements IEventIngestor {
|
||||
* Query durable events from storage.
|
||||
*/
|
||||
async query(filter: EventFilter): Promise<RuntimeEvent[]> {
|
||||
const store = await getEventStore()
|
||||
return store.query(filter)
|
||||
return this.event_store.query(filter)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,12 +206,77 @@ export class EventIngestorImpl implements IEventIngestor {
|
||||
}
|
||||
}
|
||||
|
||||
// Default singleton - also export as EventIngestor for compatibility
|
||||
export const eventIngestor = new EventIngestorImpl()
|
||||
|
||||
// Alias for backward compatibility (class — usable as both type and value)
|
||||
export const EventIngestor = EventIngestorImpl
|
||||
export type EventIngestor = EventIngestorImpl
|
||||
|
||||
// Export type for consumers
|
||||
export type { EventPersistence } from './EventSchemaRegistry.js'
|
||||
export type { EventPersistence } from './EventSchemaRegistry.js'
|
||||
|
||||
// =============================================================================
|
||||
// Process-bound ingestor binding (round5 Wf-A A.3 mitigation)
|
||||
//
|
||||
// The module-level singleton is gone. SessionManager.open_session sets the
|
||||
// process-bound ingestor exactly once per session. Any code that still
|
||||
// imports `eventIngestor` and calls it BEFORE a session is opened will
|
||||
// receive a clear "no session bound" error, surfacing the architectural
|
||||
// bypass instead of silently writing to a no-op store.
|
||||
// =============================================================================
|
||||
|
||||
let _bound_ingestor: EventIngestorImpl | null = null
|
||||
|
||||
export function bindEventIngestor(ingestor: EventIngestorImpl): void {
|
||||
_bound_ingestor = ingestor
|
||||
}
|
||||
|
||||
export function unbindEventIngestor(): void {
|
||||
_bound_ingestor = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session-bound EventIngestorImpl set by SessionManager.open_session.
|
||||
* Throws if no session has been opened in this process yet.
|
||||
*
|
||||
* Round5 Wf-A note: this is a compatibility shim, not the canonical access
|
||||
* path. New code should receive the ingestor via constructor / RuntimeApp.
|
||||
*/
|
||||
export function getEventIngestor(): EventIngestorImpl {
|
||||
if (!_bound_ingestor) {
|
||||
throw new Error(
|
||||
'EventIngestor: no session is bound. SessionManager.open_session() must be ' +
|
||||
'called before any event can be ingested. This guards against the round5 ' +
|
||||
'C-2 bypass where a module-level singleton wrote to a never-bound EventStore.'
|
||||
)
|
||||
}
|
||||
return _bound_ingestor
|
||||
}
|
||||
|
||||
/**
|
||||
* Noop ingestor: silently discards events when no session is available.
|
||||
* Used during pre-session operations like init, where event persistence is not needed.
|
||||
*/
|
||||
const noopIngestor: EventIngestorImpl = {
|
||||
ingest: async () => {},
|
||||
flush: async () => {},
|
||||
pending: () => 0,
|
||||
} as unknown as EventIngestorImpl
|
||||
|
||||
/**
|
||||
* Backward-compat shim: returns a proxy that delegates to the bound ingestor.
|
||||
* If no session is bound, returns a noop ingestor (silently discards events).
|
||||
* This allows pre-session operations like init to use ToolRegistry without noise.
|
||||
*/
|
||||
function resolveBoundOrNoop(): EventIngestorImpl {
|
||||
return _bound_ingestor ?? noopIngestor
|
||||
}
|
||||
|
||||
export const eventIngestor: EventIngestorImpl = new Proxy({} as EventIngestorImpl, {
|
||||
get(_target, prop) {
|
||||
const target = resolveBoundOrNoop() as unknown as Record<string | symbol, unknown>
|
||||
const value = target[prop]
|
||||
if (typeof value === 'function') {
|
||||
return (value as (...args: unknown[]) => unknown).bind(target)
|
||||
}
|
||||
return value
|
||||
},
|
||||
}) as EventIngestorImpl
|
||||
Reference in New Issue
Block a user