Persist normalized task symbol declarations and resolve symbols solely from durable task data. - Add declared-symbol parsing, normalization, and durable TaskStore resolution APIs. - Persist declarations through PostgreSQL migration, task serialization, and archive/restore flows. - Cover declaration precedence and schema upgrades with core tests. Files changed: docs/architecture.md | 1 + docs/storage.md | 1 + .../src/__tests__/postgres/schema-applier.test.ts | 39 +++- .../src/__tests__/task-symbol-resolution.test.ts | 222 +++++++++++++++++++++ packages/core/src/index.ts | 10 + .../core/src/postgres/migrations/0000_initial.sql | 1 + .../migrations/0028_task_declared_symbols.sql | 1 + packages/core/src/postgres/schema-applier.ts | 12 +- packages/core/src/postgres/schema/project.ts | 1 + packages/core/src/store.ts | 22 +- .../core/src/task-store/archive-lifecycle-2.ts | 2 + packages/core/src/task-store/persistence.ts | 4 +- packages/core/src/task-store/remaining-ops-2.ts | 2 +- packages/core/src/task-store/serialization.ts | 2 + packages/core/src/task-store/task-creation.ts | 5 + packages/core/src/task-store/task-row-mappers.ts | 2 +- packages/core/src/task-store/task-update.ts | 9 + packages/core/src/task-symbol-resolution.ts | 65 ++++++ packages/core/src/types.ts | 5 + 19 files changed, 400 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8405 Fusion-Task-Lineage: ca86dbe5-cd4c-4f58-b607-678d669875af Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
66 lines
3.6 KiB
TypeScript
66 lines
3.6 KiB
TypeScript
import type { Task } from "./types.js";
|
|
import { normalizeSymbolLockKey } from "./task-store/symbol-locks.js";
|
|
|
|
/**
|
|
* FNXC:SymbolLock 2026-07-31-10:00:
|
|
* FN-8305 normalizes caller-provided locks but tasks need a durable declaration
|
|
* source. File Scope is deliberately never converted into symbols: overlapping
|
|
* paths may still contain disjoint declarations. Write hydration is presence-based
|
|
* and store resolution is durable-only so an explicit clear cannot be undone by
|
|
* re-reading PROMPT.md. Work items resolve through their owning task in TaskStore.
|
|
*/
|
|
export type TaskSymbolResolutionSource = "declared" | "prompt" | "none";
|
|
export type TaskSymbolResolution =
|
|
| { resolvable: true; symbols: string[]; source: Exclude<TaskSymbolResolutionSource, "none"> }
|
|
| { resolvable: false; symbols: []; source: TaskSymbolResolutionSource; reason: "empty" | "missing-task" | "invalid-only" };
|
|
|
|
export function hasOwnDeclaredSymbols(obj: object): boolean {
|
|
return Object.prototype.hasOwnProperty.call(obj, "declaredSymbols");
|
|
}
|
|
|
|
export function normalizeDeclaredSymbols(raw: readonly string[]): string[] {
|
|
const values = new Set<string>();
|
|
for (const symbol of raw) {
|
|
try { values.add(normalizeSymbolLockKey(symbol)); } catch { /* fail-soft declaration lists */ }
|
|
}
|
|
return [...values].sort();
|
|
}
|
|
|
|
export function extractDeclaredSymbolsFromPrompt(content: string): string[] {
|
|
const heading = content.match(/^##\s+Declared\s+Symbols\s*$/m);
|
|
if (!heading) return [];
|
|
const rest = content.slice(heading.index! + heading[0].length);
|
|
const next = rest.search(/\n##?\s/);
|
|
return Array.from((next === -1 ? rest : rest.slice(0, next)).matchAll(/`([^`]+)`/g), match => match[1]);
|
|
}
|
|
|
|
/** Own property absence alone permits create-time prompt hydration. */
|
|
export function resolveCreateDeclaredSymbols(input: object, promptContent?: string | null): string[] | undefined {
|
|
if (hasOwnDeclaredSymbols(input)) {
|
|
const raw = (input as { declaredSymbols?: string[] }).declaredSymbols;
|
|
const normalized = normalizeDeclaredSymbols(Array.isArray(raw) ? raw : []);
|
|
return normalized.length ? normalized : undefined;
|
|
}
|
|
const normalized = promptContent ? normalizeDeclaredSymbols(extractDeclaredSymbolsFromPrompt(promptContent)) : [];
|
|
return normalized.length ? normalized : undefined;
|
|
}
|
|
|
|
/** Offline-only source composition; creation must use resolveCreateDeclaredSymbols. */
|
|
export function resolveTaskSymbolsFromSources(input: { declaredSymbols?: readonly string[] | null; promptContent?: string | null }): TaskSymbolResolution {
|
|
const declaredRaw = input.declaredSymbols ?? [];
|
|
const declared = normalizeDeclaredSymbols(declaredRaw);
|
|
if (declared.length) return { resolvable: true, symbols: declared, source: "declared" };
|
|
const promptRaw = input.promptContent ? extractDeclaredSymbolsFromPrompt(input.promptContent) : [];
|
|
const prompt = normalizeDeclaredSymbols(promptRaw);
|
|
if (prompt.length) return { resolvable: true, symbols: prompt, source: "prompt" };
|
|
if (declaredRaw.length > 0) return { resolvable: false, symbols: [], source: "declared", reason: "invalid-only" };
|
|
if (promptRaw.length > 0) return { resolvable: false, symbols: [], source: "prompt", reason: "invalid-only" };
|
|
return { resolvable: false, symbols: [], source: "none", reason: "empty" };
|
|
}
|
|
|
|
/** Resolves only the durable field; callers must not pass prompt content. */
|
|
export function resolveTaskSymbolsForTask(task: Pick<Task, "declaredSymbols"> | null): TaskSymbolResolution {
|
|
if (!task) return { resolvable: false, symbols: [], source: "none", reason: "missing-task" };
|
|
return resolveTaskSymbolsFromSources({ declaredSymbols: task.declaredSymbols });
|
|
}
|