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:
AirCoding
2026-06-12 17:12:29 +08:00
parent 8f55c962bb
commit ae44be31d5
364 changed files with 46779 additions and 2812 deletions

View File

@@ -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