zam-core 0.7.0 → 0.7.2

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/dist/index.d.ts CHANGED
@@ -562,16 +562,23 @@ interface Prerequisite {
562
562
  /** A prerequisite row joined with the token it points to. */
563
563
  interface PrerequisiteWithToken extends Prerequisite {
564
564
  slug: string;
565
+ title: string;
565
566
  concept: string;
566
567
  domain: string;
567
568
  bloom_level: number;
568
569
  }
570
+ /**
571
+ * Collect all prerequisite edges as an adjacency map: child → parent set.
572
+ * Only used for cycle detection; the full graph is loaded once per
573
+ * addPrerequisite call (small N in practice).
574
+ */
575
+ declare function buildAncestorMap(db: Database): Promise<Map<string, Set<string>>>;
569
576
  /**
570
577
  * Returns true if adding edge (tokenId → requiresId) would create a cycle.
571
578
  * Uses BFS from requiresId: if tokenId is reachable, adding the edge closes
572
579
  * a loop.
573
580
  */
574
- declare function wouldCreateCycle(db: Database, tokenId: string, requiresId: string): Promise<boolean>;
581
+ declare function wouldCreateCycle(db: Database, tokenId: string, requiresId: string, ancestors?: Map<string, Set<string>>): Promise<boolean>;
575
582
  /**
576
583
  * Add a prerequisite edge: tokenId requires requiresId.
577
584
  *
@@ -597,6 +604,7 @@ declare function getDependents(db: Database, tokenId: string): Promise<Prerequis
597
604
  interface NeighborhoodToken {
598
605
  id: string;
599
606
  slug: string;
607
+ title: string;
600
608
  concept: string;
601
609
  domain: string;
602
610
  bloom_level: number;
@@ -796,6 +804,7 @@ type SymbiosisMode = "shadowing" | "copilot" | "autonomy";
796
804
  interface Token {
797
805
  id: string;
798
806
  slug: string;
807
+ title: string;
799
808
  concept: string;
800
809
  domain: string;
801
810
  bloom_level: BloomLevel$1;
@@ -811,6 +820,7 @@ interface Token {
811
820
  }
812
821
  interface CreateTokenInput {
813
822
  slug: string;
823
+ title?: string;
814
824
  concept: string;
815
825
  domain?: string;
816
826
  bloom_level?: BloomLevel$1;
@@ -822,6 +832,7 @@ interface CreateTokenInput {
822
832
  topic_id?: string | null;
823
833
  }
824
834
  interface UpdateTokenInput {
835
+ title?: string | null;
825
836
  concept?: string;
826
837
  domain?: string;
827
838
  bloom_level?: BloomLevel$1;
@@ -834,6 +845,11 @@ interface UpdateTokenInput {
834
845
  }
835
846
  interface ListTokensOptions {
836
847
  domain?: string;
848
+ /**
849
+ * Filter by domain prefix using `/` as separator (e.g. "docuware-cops").
850
+ * Matches exact or startsWith(prefix + "/").
851
+ */
852
+ domainPrefix?: string;
837
853
  }
838
854
  interface TokenDeleteImpact {
839
855
  cards: number;
@@ -909,6 +925,7 @@ declare function listTokens(db: Database, options?: ListTokensOptions): Promise<
909
925
  interface PersonalCard {
910
926
  tokenId: string;
911
927
  slug: string;
928
+ title: string;
912
929
  concept: string;
913
930
  domain: string;
914
931
  bloomLevel: BloomLevel$1;
@@ -932,6 +949,18 @@ interface PersonalCard {
932
949
  topicId: string | null;
933
950
  }
934
951
  declare function slugify(text: string): string;
952
+ /**
953
+ * Strip domain prefix (using / separator) from slug for display.
954
+ */
955
+ declare function getShortSlug(slug: string, domainPrefix?: string | null): string;
956
+ /**
957
+ * Primary display name for a token: human title if present, else short slug.
958
+ * Never falls back to concept (which is a spoiler).
959
+ */
960
+ declare function getDisplayTitle(t: {
961
+ title?: string | null;
962
+ slug: string;
963
+ }, activeDomainScope?: string | null): string;
935
964
  declare function generateTokenSlug(db: Database, domain: string, concept: string, question?: string | null): Promise<string>;
936
965
  declare function listPersonalCards(db: Database, userId: string, options?: {
937
966
  query?: string;
@@ -940,6 +969,7 @@ declare function listPersonalCards(db: Database, userId: string, options?: {
940
969
  interface CurriculumCardInput {
941
970
  question: string;
942
971
  concept: string;
972
+ title?: string;
943
973
  domain: string;
944
974
  source_link?: string | null;
945
975
  context?: string;
@@ -976,6 +1006,7 @@ interface FoundationProposalInput {
976
1006
  question: string;
977
1007
  concept: string;
978
1008
  domain: string;
1009
+ title?: string;
979
1010
  context?: string;
980
1011
  bloom_level?: number;
981
1012
  symbiosis_mode?: string | null;
@@ -996,6 +1027,7 @@ interface SourceProposalInput {
996
1027
  question: string;
997
1028
  concept: string;
998
1029
  domain: string;
1030
+ title?: string;
999
1031
  bloom_level: number;
1000
1032
  symbiosis_mode: string;
1001
1033
  excerpt: string;
@@ -1048,7 +1080,9 @@ interface EmbeddedTokenRow {
1048
1080
  * vector derives from exactly this string — never the slug, which is an
1049
1081
  * identifier, not meaning.
1050
1082
  */
1051
- declare function embeddingContentForToken(t: Pick<Token, "concept" | "question" | "domain">): string;
1083
+ declare function embeddingContentForToken(t: Pick<Token, "concept" | "question" | "domain"> & {
1084
+ title?: string | null;
1085
+ }): string;
1052
1086
  declare function computeContentHash(text: string): string;
1053
1087
  /**
1054
1088
  * Encode a vector as a little-endian float32 BLOB. Builds a fresh buffer (no
@@ -1849,6 +1883,7 @@ interface ReviewQueueItem {
1849
1883
  cardId: string;
1850
1884
  tokenId: string;
1851
1885
  slug: string;
1886
+ title: string;
1852
1887
  concept: string;
1853
1888
  domain: string;
1854
1889
  bloomLevel: number;
@@ -1910,6 +1945,39 @@ declare function cosineSimilarity(a: Float32Array, b: Float32Array): number;
1910
1945
  */
1911
1946
  declare function searchTokensHybrid(db: Database, query: string, opts?: HybridSearchOptions): Promise<HybridScoredToken[]>;
1912
1947
 
1948
+ /**
1949
+ * Foundation suggestions for tokens.
1950
+ * Suggests existing tokens that are semantically related as prerequisite candidates.
1951
+ *
1952
+ * This module is pure math + database queries — zero LLM dependencies, no HTTP.
1953
+ */
1954
+
1955
+ interface FoundationSuggestion {
1956
+ token: Token;
1957
+ similarity: number;
1958
+ alreadyPrerequisite: boolean;
1959
+ wouldCreateCycle: boolean;
1960
+ /** Candidate's bloom_level is higher than the target's — unusual for a foundation. */
1961
+ bloomAboveTarget: boolean;
1962
+ }
1963
+ interface SuggestFoundationsOptions {
1964
+ queryEmbedding: ArrayLike<number>;
1965
+ /** Canonical embedding model id the stored vectors must match. */
1966
+ model: string;
1967
+ /** Set when the target token already exists (register-after / rating-1 flow). */
1968
+ targetTokenId?: string;
1969
+ /** Used for the bloomAboveTarget flag; defaults to 5 (nothing flagged). */
1970
+ targetBloomLevel?: BloomLevel$1;
1971
+ limit?: number;
1972
+ minSimilarity?: number;
1973
+ maxSimilarity?: number;
1974
+ }
1975
+ /**
1976
+ * Suggests existing tokens that are semantically related as prerequisite candidates.
1977
+ * Filters by similarity range and ranks descending by similarity.
1978
+ */
1979
+ declare function suggestFoundations(db: Database, opts: SuggestFoundationsOptions): Promise<FoundationSuggestion[]>;
1980
+
1913
1981
  /**
1914
1982
  * Get the path to ZAM's internal package SKILL.md.
1915
1983
  */
@@ -2194,4 +2262,4 @@ declare function resolveAllBeliefPaths(db: Database): Promise<string[]>;
2194
2262
  */
2195
2263
  declare function resolveAllGoalPaths(db: Database): Promise<string[]>;
2196
2264
 
2197
- export { type ADOConfig, type ADOCredentials, type AgentSkill, type AnalysisResult, type ApplySessionSynthesisInput, type ApplySessionSynthesisResult, BUILT_IN_SENSITIVE_MATCHERS, type BloomLevel$1 as BloomLevel, type CaptureDecision, type CaptureDenialReason, type CaptureRequest, type Card, type CardDeletionImpact, type CardState$1 as CardState, type CascadeBlockResult, type CommandRecord, type CommandSequence, type ConfirmFoundationsResult, type ConnectionOptions, type CreateAgentSkillInput, type CreateGoalInput, type CreateReviewInput, type CreateSessionInput, type CreateTokenInput, type Credentials, type CurriculumCardInput, DEFAULT_OBSERVER_POLICY, DEFAULT_REVIEW_CONTEXT_MAX_CHARS, type Database, type DatabaseProvider, type DatabaseTargetInfo, type DatabaseValue, type DeleteCardResult, type DeleteTokenResult, type DiscoveryOptions, type DomainCompetence, type EmbeddedTokenRow, type EmbeddingCoverage, type EmbeddingStaleness, type EvaluateInput, type EvaluateResult, type ExecuteReviewActionInput, type ExecutionContext, type FSRSParameters, type FoundationProposalInput, type Goal, type GoalFrontmatter, type GoalStatus, type GoalSummary, HOMEBREW_CASK, type HybridScoredToken, type HybridSearchOptions, type ImportCurriculumResult, type ImportResult, type InstallChannel, type InstallConfig, type InstallMode, type InstallPlan, type InstallResult, type LocalLLMRunner, type LogStepInput, type MachineAiConfig, type MachineProviderRecord, type MachineRoleBinding, type MonitorEvent, type Neighborhood, type NeighborhoodToken, OBSERVER_POLICY_UNSET_HINT, OBSERVER_POLICY_VERSION, type ObservationRating, type ObserverConsent, type ObserverPolicy, type ObserverRetention, type ObserverScope, type ObserverSettingKey, type OllamaDetectionOptions, type PersonalCard, type PrepareSessionSynthesisInput, type Prerequisite, type PrerequisiteWithToken, type PromptInput, REVIEW_CONTEXT_CACHE_TTL_MS, type Rating, type RecallPrompt, type RemoteDatabaseOptions, type RepoPaths, type ResolvedCaptureTarget, type ResolvedReference, type ReviewActionResult, type ReviewActionType, type ReviewContext, type ReviewLog, type ReviewQueue, type ReviewQueueItem, type ReviewQueueOptions, type RunResult, SIDECAR_POLICY_FILE, SNAPSHOT_VERSION, type SchedulingCard, type Session, type SessionStep, type SessionSummary, type SessionSynthesisCandidate, type SessionSynthesisEvidence, type SessionSynthesisPreview, type SessionSynthesisRecord, type SidecarPrivacyPolicy, type SkillProposal, type SkillSource, type SnapshotManifest, type SourceProposalInput, type SplitProposalInput, type Statement, type SupportedLocale, type SymbiosisMode, type SynthesisConfidence, type SystemProfile, type Token, type TokenDeleteImpact, type TokenEmbedding, type TokenNeedingEmbedding, type TokenPattern, type TranslationKey, type TursoCredentials, UI_OBSERVATION_PROTOCOL_VERSION, type UiActionType, type UiApplicationContext, type UiCandidateToken, type UiEvidenceRef, type UiEvidenceType, type UiObservationKind, type UiObservationReport, type UiObservedAction, type UnblockResult, type UpdateActionKind, type UpdateCardInput, type UpdateDecision, type UpdateStep, type UpdateStepKind, type UpdateTokenInput, type UserSetting, type UserStats, WINGET_PACKAGE_ID, type WorkItem, type WorkspaceConfig, type WorkspaceKind, type WorkspaceSourceControl, addPrerequisite, analyzeObservation, appendUiObservationReport, applySessionSynthesis, buildReviewQueue, buildUiSynthesisCandidates, cascadeBlock, clearADOCredentials, clearProviderApiKey, clearReviewContextCache, clearTursoCredentials, compareVersions, computeContentHash, confirmCardSplit, confirmFoundations, confirmSourceImport, cosineSimilarity, createAgentSkill, createFSRS, createGoal, createToken, decidePostCapture, decidePreCapture, decideUpdate, decodeEmbedding, deleteCardForUser, deleteSetting, deleteToken, deprecateToken, detectSyncProvider, detectSystemLocale, discoverSkills, distributeGlobalSkills, embeddingContentForToken, encodeEmbedding, endSession, ensureCard, ensureMonitorDir, ensureUiObserverDir, evaluateRating, executeReviewAction, exportSnapshot, extractTasks, extractTokenRefs, fetchActiveWorkItems, findTokens, generateBashHooks, generateBashUnhooks, generateConceptFreeCue, generatePowerShellHooks, generatePowerShellUnhooks, generatePrompt, generateTokenSlug, generateZshHooks, generateZshUnhooks, getADOCredentials, getActiveWorkspace, getActiveWorkspaceId, getAgentSkill, getAllSettings, getAllSettingsDetailed, getBlockedCards, getCard, getCardById, getCardDeletionImpact, getConfiguredWorkspaces, getDatabaseTargetInfo, getDefaultDbPath, getDependents, getDomainCompetence, getDueCards, getEmbeddingCoverage, getGoal, getGoalTree, getInstallChannel, getInstallMode, getMachineAiConfig, getMonitorDir, getMonitorLogStats, getMonitorPath, getPackageSkillPath, getPrerequisites, getProviderApiKey, getRepoPaths, getReviewsForCard, getReviewsForUser, getSessionSummary, getSessionSynthesisRecords, getSetting, getSystemProfile, getTokenById, getTokenBySlug, getTokenDeleteImpact, getTokenEmbedding, getTokenNeighborhood, getTursoCredentials, getUiObservationPath, getUiObserverDir, getUserStats, hasCommand, importCurriculumCards, importSnapshot, injectShellHooks, installFastFlowLM, installOllama, installOpenCode, interleave, isObserverPolicyConfigured, isOllamaInstalled, isUiObservationReport, listAgentSkills, listEmbeddedTokens, listGoals, listPersonalCards, listProviderApiKeyRefs, listTokens, listTokensNeedingEmbedding, loadADOConfig, loadCredentials, loadInstallConfig, logReview, logStep, matchBuiltInSensitive, matchDenylist, matchesFilePath, monitorLogExists, normalizeLocale, normalizePath, openDatabase, openDatabaseWithSync, openRemoteDatabase, pairCommands, parseGoalFile, parseMonitorLog, parseObserverList, parseObserverPolicy, parseSnapshot, parseUiObservationLog, planOpenCodeInstall, planUpdate, prepareLocalModel, prepareSessionSynthesis, readMonitorLog, readUiObservationLog, removeConfiguredWorkspace, resolveAllBeliefPaths, resolveAllGoalPaths, resolveObserverPolicy, resolveOllamaCommand, resolveReference, resolveRepoPath, resolveReviewContext, saveConfiguredWorkspaces, saveCredentials, saveInstallConfig, saveMachineAiConfig, searchTokensHybrid, serializeGoal, setADOCredentials, setActiveWorkspaceId, setInstallChannel, setInstallMode, setProviderApiKey, setSetting, setTursoCredentials, slugify, startSession, syncObserverSidecarPolicy, t, toSidecarPrivacyPolicy, uiObservationLogExists, uiObservationTimeSpan, unblockReady, updateCard, updateGoalStatus, updateToken, upsertConfiguredWorkspace, upsertTokenEmbedding, verifySnapshot, wouldCreateCycle, writeMonitorEvent };
2265
+ export { type ADOConfig, type ADOCredentials, type AgentSkill, type AnalysisResult, type ApplySessionSynthesisInput, type ApplySessionSynthesisResult, BUILT_IN_SENSITIVE_MATCHERS, type BloomLevel$1 as BloomLevel, type CaptureDecision, type CaptureDenialReason, type CaptureRequest, type Card, type CardDeletionImpact, type CardState$1 as CardState, type CascadeBlockResult, type CommandRecord, type CommandSequence, type ConfirmFoundationsResult, type ConnectionOptions, type CreateAgentSkillInput, type CreateGoalInput, type CreateReviewInput, type CreateSessionInput, type CreateTokenInput, type Credentials, type CurriculumCardInput, DEFAULT_OBSERVER_POLICY, DEFAULT_REVIEW_CONTEXT_MAX_CHARS, type Database, type DatabaseProvider, type DatabaseTargetInfo, type DatabaseValue, type DeleteCardResult, type DeleteTokenResult, type DiscoveryOptions, type DomainCompetence, type EmbeddedTokenRow, type EmbeddingCoverage, type EmbeddingStaleness, type EvaluateInput, type EvaluateResult, type ExecuteReviewActionInput, type ExecutionContext, type FSRSParameters, type FoundationProposalInput, type FoundationSuggestion, type Goal, type GoalFrontmatter, type GoalStatus, type GoalSummary, HOMEBREW_CASK, type HybridScoredToken, type HybridSearchOptions, type ImportCurriculumResult, type ImportResult, type InstallChannel, type InstallConfig, type InstallMode, type InstallPlan, type InstallResult, type ListTokensOptions, type LocalLLMRunner, type LogStepInput, type MachineAiConfig, type MachineProviderRecord, type MachineRoleBinding, type MonitorEvent, type Neighborhood, type NeighborhoodToken, OBSERVER_POLICY_UNSET_HINT, OBSERVER_POLICY_VERSION, type ObservationRating, type ObserverConsent, type ObserverPolicy, type ObserverRetention, type ObserverScope, type ObserverSettingKey, type OllamaDetectionOptions, type PersonalCard, type PrepareSessionSynthesisInput, type Prerequisite, type PrerequisiteWithToken, type PromptInput, REVIEW_CONTEXT_CACHE_TTL_MS, type Rating, type RecallPrompt, type RemoteDatabaseOptions, type RepoPaths, type ResolvedCaptureTarget, type ResolvedReference, type ReviewActionResult, type ReviewActionType, type ReviewContext, type ReviewLog, type ReviewQueue, type ReviewQueueItem, type ReviewQueueOptions, type RunResult, SIDECAR_POLICY_FILE, SNAPSHOT_VERSION, type SchedulingCard, type Session, type SessionStep, type SessionSummary, type SessionSynthesisCandidate, type SessionSynthesisEvidence, type SessionSynthesisPreview, type SessionSynthesisRecord, type SidecarPrivacyPolicy, type SkillProposal, type SkillSource, type SnapshotManifest, type SourceProposalInput, type SplitProposalInput, type Statement, type SuggestFoundationsOptions, type SupportedLocale, type SymbiosisMode, type SynthesisConfidence, type SystemProfile, type Token, type TokenDeleteImpact, type TokenEmbedding, type TokenNeedingEmbedding, type TokenPattern, type TranslationKey, type TursoCredentials, UI_OBSERVATION_PROTOCOL_VERSION, type UiActionType, type UiApplicationContext, type UiCandidateToken, type UiEvidenceRef, type UiEvidenceType, type UiObservationKind, type UiObservationReport, type UiObservedAction, type UnblockResult, type UpdateActionKind, type UpdateCardInput, type UpdateDecision, type UpdateStep, type UpdateStepKind, type UpdateTokenInput, type UserSetting, type UserStats, WINGET_PACKAGE_ID, type WorkItem, type WorkspaceConfig, type WorkspaceKind, type WorkspaceSourceControl, addPrerequisite, analyzeObservation, appendUiObservationReport, applySessionSynthesis, buildAncestorMap, buildReviewQueue, buildUiSynthesisCandidates, cascadeBlock, clearADOCredentials, clearProviderApiKey, clearReviewContextCache, clearTursoCredentials, compareVersions, computeContentHash, confirmCardSplit, confirmFoundations, confirmSourceImport, cosineSimilarity, createAgentSkill, createFSRS, createGoal, createToken, decidePostCapture, decidePreCapture, decideUpdate, decodeEmbedding, deleteCardForUser, deleteSetting, deleteToken, deprecateToken, detectSyncProvider, detectSystemLocale, discoverSkills, distributeGlobalSkills, embeddingContentForToken, encodeEmbedding, endSession, ensureCard, ensureMonitorDir, ensureUiObserverDir, evaluateRating, executeReviewAction, exportSnapshot, extractTasks, extractTokenRefs, fetchActiveWorkItems, findTokens, generateBashHooks, generateBashUnhooks, generateConceptFreeCue, generatePowerShellHooks, generatePowerShellUnhooks, generatePrompt, generateTokenSlug, generateZshHooks, generateZshUnhooks, getADOCredentials, getActiveWorkspace, getActiveWorkspaceId, getAgentSkill, getAllSettings, getAllSettingsDetailed, getBlockedCards, getCard, getCardById, getCardDeletionImpact, getConfiguredWorkspaces, getDatabaseTargetInfo, getDefaultDbPath, getDependents, getDisplayTitle, getDomainCompetence, getDueCards, getEmbeddingCoverage, getGoal, getGoalTree, getInstallChannel, getInstallMode, getMachineAiConfig, getMonitorDir, getMonitorLogStats, getMonitorPath, getPackageSkillPath, getPrerequisites, getProviderApiKey, getRepoPaths, getReviewsForCard, getReviewsForUser, getSessionSummary, getSessionSynthesisRecords, getSetting, getShortSlug, getSystemProfile, getTokenById, getTokenBySlug, getTokenDeleteImpact, getTokenEmbedding, getTokenNeighborhood, getTursoCredentials, getUiObservationPath, getUiObserverDir, getUserStats, hasCommand, importCurriculumCards, importSnapshot, injectShellHooks, installFastFlowLM, installOllama, installOpenCode, interleave, isObserverPolicyConfigured, isOllamaInstalled, isUiObservationReport, listAgentSkills, listEmbeddedTokens, listGoals, listPersonalCards, listProviderApiKeyRefs, listTokens, listTokensNeedingEmbedding, loadADOConfig, loadCredentials, loadInstallConfig, logReview, logStep, matchBuiltInSensitive, matchDenylist, matchesFilePath, monitorLogExists, normalizeLocale, normalizePath, openDatabase, openDatabaseWithSync, openRemoteDatabase, pairCommands, parseGoalFile, parseMonitorLog, parseObserverList, parseObserverPolicy, parseSnapshot, parseUiObservationLog, planOpenCodeInstall, planUpdate, prepareLocalModel, prepareSessionSynthesis, readMonitorLog, readUiObservationLog, removeConfiguredWorkspace, resolveAllBeliefPaths, resolveAllGoalPaths, resolveObserverPolicy, resolveOllamaCommand, resolveReference, resolveRepoPath, resolveReviewContext, saveConfiguredWorkspaces, saveCredentials, saveInstallConfig, saveMachineAiConfig, searchTokensHybrid, serializeGoal, setADOCredentials, setActiveWorkspaceId, setInstallChannel, setInstallMode, setProviderApiKey, setSetting, setTursoCredentials, slugify, startSession, suggestFoundations, syncObserverSidecarPolicy, t, toSidecarPrivacyPolicy, uiObservationLogExists, uiObservationTimeSpan, unblockReady, updateCard, updateGoalStatus, updateToken, upsertConfiguredWorkspace, upsertTokenEmbedding, verifySnapshot, wouldCreateCycle, writeMonitorEvent };
package/dist/index.js CHANGED
@@ -530,6 +530,7 @@ var SCHEMA = `
530
530
  CREATE TABLE IF NOT EXISTS tokens (
531
531
  id TEXT PRIMARY KEY,
532
532
  slug TEXT UNIQUE NOT NULL,
533
+ title TEXT NOT NULL DEFAULT '',
533
534
  concept TEXT NOT NULL,
534
535
  domain TEXT NOT NULL DEFAULT '',
535
536
  bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
@@ -678,6 +679,8 @@ CREATE INDEX IF NOT EXISTS idx_cards_token_user ON cards(token_id, user_id);
678
679
  CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
679
680
  CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
680
681
  CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
682
+ CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title);
683
+ CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
681
684
  `;
682
685
 
683
686
  // src/kernel/db/sync-adapter.ts
@@ -1011,6 +1014,15 @@ async function runMigrations(db) {
1011
1014
  embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
1012
1015
  )
1013
1016
  `);
1017
+ if (!tokenCols.some((c) => c.name === "title")) {
1018
+ await db.exec(
1019
+ `ALTER TABLE tokens ADD COLUMN title TEXT NOT NULL DEFAULT ''`
1020
+ );
1021
+ }
1022
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title)`);
1023
+ await db.exec(
1024
+ `CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain)`
1025
+ );
1014
1026
  }
1015
1027
 
1016
1028
  // src/kernel/db/snapshot.ts
@@ -1512,12 +1524,14 @@ async function createToken(db, input) {
1512
1524
  if (bloom < 1 || bloom > 5) {
1513
1525
  throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1514
1526
  }
1527
+ const title = input.title ?? "";
1515
1528
  await db.prepare(`
1516
- INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
1517
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1529
+ INSERT INTO tokens (id, slug, title, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
1530
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1518
1531
  `).run(
1519
1532
  id,
1520
1533
  input.slug,
1534
+ title,
1521
1535
  input.concept,
1522
1536
  input.domain ?? "",
1523
1537
  bloom,
@@ -1560,6 +1574,10 @@ async function updateToken(db, slug, updates) {
1560
1574
  }
1561
1575
  const fields = [];
1562
1576
  const values = [];
1577
+ if (updates.title !== void 0) {
1578
+ fields.push("title = ?");
1579
+ values.push(updates.title ?? "");
1580
+ }
1563
1581
  if (updates.concept !== void 0) {
1564
1582
  fields.push("concept = ?");
1565
1583
  values.push(updates.concept);
@@ -1665,13 +1683,14 @@ async function deleteToken(db, slug) {
1665
1683
  await db.transaction(async (tx) => {
1666
1684
  const now = (/* @__PURE__ */ new Date()).toISOString();
1667
1685
  const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
1686
+ const skillUpdateStmt = tx.prepare(
1687
+ "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1688
+ );
1668
1689
  for (const row of skillRows) {
1669
1690
  const tokenSlugs = JSON.parse(row.token_slugs);
1670
1691
  const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
1671
1692
  if (filtered.length !== tokenSlugs.length) {
1672
- await tx.prepare(
1673
- "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1674
- ).run(JSON.stringify(filtered), now, row.id);
1693
+ await skillUpdateStmt.run(JSON.stringify(filtered), now, row.id);
1675
1694
  }
1676
1695
  }
1677
1696
  await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
@@ -1685,10 +1704,16 @@ async function findTokens(db, query) {
1685
1704
  const shortTerms = searchTokens.filter((t2) => t2.length <= 2);
1686
1705
  const longTerms = searchTokens.filter((t2) => t2.length > 2);
1687
1706
  const scoreMap = /* @__PURE__ */ new Map();
1688
- const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
1707
+ const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(title) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
1708
+ const likeStmt = db.prepare(likeSQL);
1689
1709
  for (const term of longTerms) {
1690
1710
  const pattern = `%${term}%`;
1691
- const rows = await db.prepare(likeSQL).all(pattern, pattern, pattern);
1711
+ const rows = await likeStmt.all(
1712
+ pattern,
1713
+ pattern,
1714
+ pattern,
1715
+ pattern
1716
+ );
1692
1717
  for (const row of rows) {
1693
1718
  const entry = scoreMap.get(row.id);
1694
1719
  if (entry) {
@@ -1701,7 +1726,7 @@ async function findTokens(db, query) {
1701
1726
  if (shortTerms.length > 0 || longTerms.length === 0) {
1702
1727
  const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
1703
1728
  for (const token of allTokens) {
1704
- const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1729
+ const words = `${token.slug} ${token.title} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1705
1730
  let matchCount = 0;
1706
1731
  for (const term of shortTerms.length > 0 ? shortTerms : searchTokens) {
1707
1732
  for (const w of words) {
@@ -1735,6 +1760,11 @@ async function listTokens(db, options) {
1735
1760
  tokens = await db.prepare(
1736
1761
  "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
1737
1762
  ).all(options.domain);
1763
+ } else if (options?.domainPrefix) {
1764
+ const prefix = options.domainPrefix;
1765
+ tokens = await db.prepare(
1766
+ `SELECT * FROM tokens WHERE (domain = ? OR domain LIKE ?) AND deprecated_at IS NULL ORDER BY bloom_level, slug`
1767
+ ).all(prefix, `${prefix}/%`);
1738
1768
  } else {
1739
1769
  tokens = await db.prepare(
1740
1770
  "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
@@ -1746,7 +1776,20 @@ async function listTokens(db, options) {
1746
1776
  return tokens;
1747
1777
  }
1748
1778
  function slugify(text) {
1749
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1779
+ return text.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1780
+ }
1781
+ function getShortSlug(slug, domainPrefix) {
1782
+ if (domainPrefix) {
1783
+ const folded = domainPrefix.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1784
+ if (folded && slug.startsWith(`${folded}-`)) {
1785
+ return slug.substring(folded.length + 1);
1786
+ }
1787
+ }
1788
+ return slug;
1789
+ }
1790
+ function getDisplayTitle(t2, activeDomainScope) {
1791
+ if (t2.title?.trim()) return t2.title.trim();
1792
+ return getShortSlug(t2.slug, activeDomainScope);
1750
1793
  }
1751
1794
  async function generateTokenSlug(db, domain, concept, question) {
1752
1795
  const baseText = question && question.trim().length > 0 ? question : concept;
@@ -1776,6 +1819,7 @@ async function listPersonalCards(db, userId, options) {
1776
1819
  SELECT
1777
1820
  t.id AS tokenId,
1778
1821
  t.slug,
1822
+ t.title,
1779
1823
  t.concept,
1780
1824
  t.domain,
1781
1825
  t.bloom_level AS bloomLevel,
@@ -1877,6 +1921,7 @@ async function importCurriculumCards(db, userId, cards) {
1877
1921
  );
1878
1922
  token = await createToken(tx, {
1879
1923
  slug: finalSlug,
1924
+ title: card.title,
1880
1925
  concept: card.concept,
1881
1926
  domain: card.domain,
1882
1927
  bloom_level: bloom,
@@ -1994,23 +2039,28 @@ async function confirmCardSplit(db, userId, originalSlug, action, originalQuesti
1994
2039
  (/* @__PURE__ */ new Date()).toISOString(),
1995
2040
  originalToken.id
1996
2041
  );
2042
+ const insertPrereqStmt = tx.prepare(
2043
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2044
+ );
1997
2045
  for (const propToken of proposalTokens) {
1998
- await tx.prepare(
1999
- "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2000
- ).run(originalToken.id, propToken.id);
2046
+ await insertPrereqStmt.run(originalToken.id, propToken.id);
2001
2047
  }
2002
2048
  await tx.prepare(
2003
2049
  "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
2004
2050
  ).run(originalToken.id, userId);
2051
+ const checkPrereqsStmt = tx.prepare(
2052
+ "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
2053
+ );
2054
+ const unblockCardStmt = tx.prepare(
2055
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?"
2056
+ );
2005
2057
  for (const propToken of proposalTokens) {
2006
2058
  const card = await ensureCard(tx, propToken.id, userId);
2007
2059
  if (card.blocked === 1) {
2008
- const prereqOfPrereq = await tx.prepare(
2009
- "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
2010
- ).get(propToken.id);
2060
+ const prereqOfPrereq = await checkPrereqsStmt.get(propToken.id);
2011
2061
  if (prereqOfPrereq.n === 0) {
2012
2062
  const now = (/* @__PURE__ */ new Date()).toISOString();
2013
- await tx.prepare("UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?").run(now, card.id);
2063
+ await unblockCardStmt.run(now, card.id);
2014
2064
  }
2015
2065
  }
2016
2066
  }
@@ -2081,6 +2131,7 @@ async function confirmFoundations(db, userId, originalSlug, proposals) {
2081
2131
  );
2082
2132
  token = await createToken(tx, {
2083
2133
  slug: finalSlug,
2134
+ title: card.title,
2084
2135
  concept: card.concept,
2085
2136
  domain: card.domain,
2086
2137
  bloom_level: bloom,
@@ -2147,6 +2198,7 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
2147
2198
  }
2148
2199
  token = await createToken(tx, {
2149
2200
  slug: finalSlug,
2201
+ title: card.title,
2150
2202
  concept: card.concept,
2151
2203
  domain: card.domain,
2152
2204
  bloom_level: bloom,
@@ -2215,9 +2267,9 @@ async function buildAncestorMap(db) {
2215
2267
  }
2216
2268
  return map;
2217
2269
  }
2218
- async function wouldCreateCycle(db, tokenId, requiresId) {
2270
+ async function wouldCreateCycle(db, tokenId, requiresId, ancestors) {
2219
2271
  if (tokenId === requiresId) return true;
2220
- const ancestors = await buildAncestorMap(db);
2272
+ const map = ancestors ?? await buildAncestorMap(db);
2221
2273
  const visited = /* @__PURE__ */ new Set();
2222
2274
  const queue = [requiresId];
2223
2275
  while (queue.length > 0) {
@@ -2225,7 +2277,7 @@ async function wouldCreateCycle(db, tokenId, requiresId) {
2225
2277
  if (current === tokenId) return true;
2226
2278
  if (visited.has(current)) continue;
2227
2279
  visited.add(current);
2228
- const parents = ancestors.get(current);
2280
+ const parents = map.get(current);
2229
2281
  if (parents) {
2230
2282
  for (const parent of parents) {
2231
2283
  if (!visited.has(parent)) queue.push(parent);
@@ -2249,7 +2301,7 @@ async function addPrerequisite(db, tokenId, requiresId) {
2249
2301
  }
2250
2302
  async function getPrerequisites(db, tokenId) {
2251
2303
  return await db.prepare(
2252
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
2304
+ `SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
2253
2305
  FROM prerequisites p
2254
2306
  JOIN tokens t ON t.id = p.requires_id
2255
2307
  WHERE p.token_id = ?`
@@ -2257,7 +2309,7 @@ async function getPrerequisites(db, tokenId) {
2257
2309
  }
2258
2310
  async function getDependents(db, tokenId) {
2259
2311
  return await db.prepare(
2260
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
2312
+ `SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
2261
2313
  FROM prerequisites p
2262
2314
  JOIN tokens t ON t.id = p.token_id
2263
2315
  WHERE p.requires_id = ?`
@@ -2288,6 +2340,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2288
2340
  const toNode = (t2, card) => ({
2289
2341
  id: t2.id,
2290
2342
  slug: t2.slug,
2343
+ title: t2.title ?? t2.slug,
2291
2344
  concept: t2.concept,
2292
2345
  domain: t2.domain,
2293
2346
  bloom_level: t2.bloom_level,
@@ -2307,6 +2360,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2307
2360
  {
2308
2361
  id: p.requires_id,
2309
2362
  slug: p.slug,
2363
+ title: p.title,
2310
2364
  concept: p.concept,
2311
2365
  domain: p.domain,
2312
2366
  bloom_level: p.bloom_level
@@ -2319,6 +2373,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2319
2373
  {
2320
2374
  id: d.token_id,
2321
2375
  slug: d.slug,
2376
+ title: d.title,
2322
2377
  concept: d.concept,
2323
2378
  domain: d.domain,
2324
2379
  bloom_level: d.bloom_level
@@ -2478,7 +2533,8 @@ import { createHash as createHash2 } from "crypto";
2478
2533
  function embeddingContentForToken(t2) {
2479
2534
  return `${t2.concept}
2480
2535
  ${t2.question ?? ""}
2481
- ${t2.domain}`;
2536
+ ${t2.domain}
2537
+ ${t2.title ?? ""}`;
2482
2538
  }
2483
2539
  function computeContentHash(text) {
2484
2540
  return createHash2("sha256").update(text, "utf8").digest("hex");
@@ -4662,6 +4718,7 @@ async function buildReviewQueue(db, options) {
4662
4718
  c.id AS card_id,
4663
4719
  c.token_id AS token_id,
4664
4720
  t.slug AS slug,
4721
+ t.title AS title,
4665
4722
  t.concept AS concept,
4666
4723
  t.domain AS domain,
4667
4724
  t.bloom_level AS bloom_level,
@@ -4683,6 +4740,7 @@ async function buildReviewQueue(db, options) {
4683
4740
  c.id AS card_id,
4684
4741
  c.token_id AS token_id,
4685
4742
  t.slug AS slug,
4743
+ t.title AS title,
4686
4744
  t.concept AS concept,
4687
4745
  t.domain AS domain,
4688
4746
  t.bloom_level AS bloom_level,
@@ -4742,6 +4800,7 @@ function rowToItem(row) {
4742
4800
  cardId: row.card_id,
4743
4801
  tokenId: row.token_id,
4744
4802
  slug: row.slug,
4803
+ title: getDisplayTitle(row),
4745
4804
  concept: row.concept,
4746
4805
  domain: row.domain,
4747
4806
  bloomLevel: row.bloom_level,
@@ -4850,6 +4909,71 @@ async function searchTokensHybrid(db, query, opts) {
4850
4909
  return results.slice(0, limit);
4851
4910
  }
4852
4911
 
4912
+ // src/kernel/search/suggestions.ts
4913
+ async function suggestFoundations(db, opts) {
4914
+ const minSimilarity = opts.minSimilarity ?? 0.45;
4915
+ const maxSimilarity = opts.maxSimilarity ?? 0.85;
4916
+ const limit = opts.limit ?? 5;
4917
+ const targetBloomLevel = opts.targetBloomLevel ?? 5;
4918
+ if (minSimilarity >= maxSimilarity) {
4919
+ return [];
4920
+ }
4921
+ const embedded = await listEmbeddedTokens(db, opts.model);
4922
+ if (embedded.length === 0) {
4923
+ return [];
4924
+ }
4925
+ const queryVec = Float32Array.from(opts.queryEmbedding);
4926
+ const candidates = [];
4927
+ for (const row of embedded) {
4928
+ if (opts.targetTokenId && row.token.id === opts.targetTokenId) {
4929
+ continue;
4930
+ }
4931
+ const similarity = cosineSimilarity(queryVec, row.embedding);
4932
+ if (similarity >= minSimilarity && similarity < maxSimilarity) {
4933
+ candidates.push({ token: row.token, similarity });
4934
+ }
4935
+ }
4936
+ candidates.sort((a, b) => {
4937
+ if (Math.abs(a.similarity - b.similarity) > 1e-9) {
4938
+ return b.similarity - a.similarity;
4939
+ }
4940
+ return a.token.slug.localeCompare(b.token.slug);
4941
+ });
4942
+ const topCandidates = candidates.slice(0, limit);
4943
+ const prereqIds = /* @__PURE__ */ new Set();
4944
+ let ancestors;
4945
+ if (opts.targetTokenId) {
4946
+ const prereqs = await getPrerequisites(db, opts.targetTokenId);
4947
+ for (const p of prereqs) {
4948
+ prereqIds.add(p.requires_id);
4949
+ }
4950
+ ancestors = await buildAncestorMap(db);
4951
+ }
4952
+ const results = [];
4953
+ for (const cand of topCandidates) {
4954
+ let alreadyPrerequisite = false;
4955
+ let wouldCreateCycleFlag = false;
4956
+ const bloomAboveTarget = cand.token.bloom_level > targetBloomLevel;
4957
+ if (opts.targetTokenId) {
4958
+ alreadyPrerequisite = prereqIds.has(cand.token.id);
4959
+ wouldCreateCycleFlag = await wouldCreateCycle(
4960
+ db,
4961
+ opts.targetTokenId,
4962
+ cand.token.id,
4963
+ ancestors
4964
+ );
4965
+ }
4966
+ results.push({
4967
+ token: cand.token,
4968
+ similarity: cand.similarity,
4969
+ alreadyPrerequisite,
4970
+ wouldCreateCycle: wouldCreateCycleFlag,
4971
+ bloomAboveTarget
4972
+ });
4973
+ }
4974
+ return results;
4975
+ }
4976
+
4853
4977
  // src/kernel/system/hooks.ts
4854
4978
  import {
4855
4979
  appendFileSync as appendFileSync3,
@@ -5874,6 +5998,7 @@ export {
5874
5998
  analyzeObservation,
5875
5999
  appendUiObservationReport,
5876
6000
  applySessionSynthesis,
6001
+ buildAncestorMap,
5877
6002
  buildReviewQueue,
5878
6003
  buildUiSynthesisCandidates,
5879
6004
  cascadeBlock,
@@ -5939,6 +6064,7 @@ export {
5939
6064
  getDatabaseTargetInfo,
5940
6065
  getDefaultDbPath,
5941
6066
  getDependents,
6067
+ getDisplayTitle,
5942
6068
  getDomainCompetence,
5943
6069
  getDueCards,
5944
6070
  getEmbeddingCoverage,
@@ -5959,6 +6085,7 @@ export {
5959
6085
  getSessionSummary,
5960
6086
  getSessionSynthesisRecords,
5961
6087
  getSetting,
6088
+ getShortSlug,
5962
6089
  getSystemProfile,
5963
6090
  getTokenById,
5964
6091
  getTokenBySlug,
@@ -6037,6 +6164,7 @@ export {
6037
6164
  setTursoCredentials,
6038
6165
  slugify,
6039
6166
  startSession,
6167
+ suggestFoundations,
6040
6168
  syncObserverSidecarPolicy,
6041
6169
  t,
6042
6170
  toSidecarPrivacyPolicy,