Files
fusion/packages/core/src/task-symbol-resolution.ts
gsxdsm 3008eb2dd8 FN-8405: add durable task symbol declarations
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>
2026-07-19 21:19:56 -07:00

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 });
}