facult 2.8.2 → 2.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "2.8.2",
3
+ "version": "2.8.3",
4
4
  "description": "Manage canonical AI capabilities, sync surfaces, and evolution state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/ai-state.ts CHANGED
@@ -196,6 +196,20 @@ export async function ensureAiIndexPath(args: {
196
196
  )) {
197
197
  if (await fileExists(legacyPath)) {
198
198
  if (args.repair !== false) {
199
+ if (
200
+ await canonicalAssetsNewerThanIndex({
201
+ homeDir: args.homeDir,
202
+ rootDir: args.rootDir,
203
+ indexPath: legacyPath,
204
+ })
205
+ ) {
206
+ const { outputPath } = await buildIndex({
207
+ rootDir: args.rootDir,
208
+ homeDir: args.homeDir,
209
+ force: false,
210
+ });
211
+ return { path: outputPath, repaired: true, source: "rebuilt" };
212
+ }
199
213
  await mkdir(dirname(generatedPath), { recursive: true });
200
214
  await copyFile(legacyPath, generatedPath);
201
215
  }
@@ -257,6 +271,20 @@ export async function ensureAiGraphPath(args: {
257
271
  )) {
258
272
  if (await fileExists(legacyGeneratedPath)) {
259
273
  if (args.repair !== false) {
274
+ if (
275
+ await canonicalAssetsNewerThanIndex({
276
+ homeDir: args.homeDir,
277
+ rootDir: args.rootDir,
278
+ indexPath: legacyGeneratedPath,
279
+ })
280
+ ) {
281
+ const { graphPath } = await buildIndex({
282
+ rootDir: args.rootDir,
283
+ homeDir: args.homeDir,
284
+ force: false,
285
+ });
286
+ return { path: graphPath, rebuilt: true };
287
+ }
260
288
  await mkdir(dirname(generatedPath), { recursive: true });
261
289
  await copyFile(legacyGeneratedPath, generatedPath);
262
290
  }
package/src/ai.ts CHANGED
@@ -256,6 +256,24 @@ function aiJournalReadPaths(homeDir: string, rootDir: string): string[] {
256
256
  ];
257
257
  }
258
258
 
259
+ function aiProposalReadDirs(homeDir: string, rootDir: string): string[] {
260
+ return uniqueStrings([
261
+ facultAiProposalDir(homeDir, rootDir),
262
+ ...legacyAiRuntimeScopeDirs(homeDir, rootDir).map((dir) =>
263
+ join(dir, "evolution", "proposals")
264
+ ),
265
+ ]);
266
+ }
267
+
268
+ async function firstExistingFile(paths: string[]): Promise<string | null> {
269
+ for (const pathValue of paths) {
270
+ if (await fileExists(pathValue)) {
271
+ return pathValue;
272
+ }
273
+ }
274
+ return null;
275
+ }
276
+
259
277
  function supportedDraftTarget(pathValue: string): boolean {
260
278
  return pathValue.toLowerCase().endsWith(".md");
261
279
  }
@@ -645,11 +663,15 @@ async function nextProposalId(
645
663
  homeDir: string,
646
664
  rootDir: string
647
665
  ): Promise<string> {
648
- const dir = facultAiProposalDir(homeDir, rootDir);
649
- const entries = await readdir(dir).catch(() => [] as string[]);
650
- const ids = entries
651
- .filter((entry) => entry.endsWith(".json"))
652
- .map((entry) => basename(entry, ".json"));
666
+ const ids: string[] = [];
667
+ for (const dir of aiProposalReadDirs(homeDir, rootDir)) {
668
+ const entries = await readdir(dir).catch(() => [] as string[]);
669
+ ids.push(
670
+ ...entries
671
+ .filter((entry) => entry.endsWith(".json"))
672
+ .map((entry) => basename(entry, ".json"))
673
+ );
674
+ }
653
675
  return nextId("EV", ids);
654
676
  }
655
677
 
@@ -842,18 +864,19 @@ export async function listProposals(args?: {
842
864
  throw new Error("listProposals requires a rootDir");
843
865
  }
844
866
  const homeDir = args?.homeDir ?? process.env.HOME ?? "";
845
- const dir = facultAiProposalDir(homeDir, args?.rootDir);
846
- const entries = await readdir(dir).catch(() => [] as string[]);
847
- const out: AiProposalRecord[] = [];
848
- for (const entry of entries.sort()) {
849
- if (!entry.endsWith(".json")) {
850
- continue;
867
+ const byId = new Map<string, AiProposalRecord>();
868
+ for (const dir of [...aiProposalReadDirs(homeDir, args.rootDir)].reverse()) {
869
+ const entries = await readdir(dir).catch(() => [] as string[]);
870
+ for (const entry of entries.sort()) {
871
+ if (!entry.endsWith(".json")) {
872
+ continue;
873
+ }
874
+ const raw = await readFile(join(dir, entry), "utf8");
875
+ const parsed = JSON.parse(raw) as AiProposalRecord;
876
+ byId.set(parsed.id, parsed);
851
877
  }
852
- const raw = await readFile(join(dir, entry), "utf8");
853
- const parsed = JSON.parse(raw) as AiProposalRecord;
854
- out.push(parsed);
855
878
  }
856
- return out;
879
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
857
880
  }
858
881
 
859
882
  export async function showProposal(
@@ -861,15 +884,15 @@ export async function showProposal(
861
884
  args: { homeDir?: string; rootDir: string }
862
885
  ): Promise<AiProposalRecord | null> {
863
886
  const homeDir = args.homeDir ?? process.env.HOME ?? "";
864
- const pathValue = join(
865
- facultAiProposalDir(homeDir, args.rootDir),
866
- `${id}.json`
867
- );
868
- if (!(await fileExists(pathValue))) {
869
- return null;
887
+ for (const dir of aiProposalReadDirs(homeDir, args.rootDir)) {
888
+ const pathValue = join(dir, `${id}.json`);
889
+ if (!(await fileExists(pathValue))) {
890
+ continue;
891
+ }
892
+ const raw = await readFile(pathValue, "utf8");
893
+ return JSON.parse(raw) as AiProposalRecord;
870
894
  }
871
- const raw = await readFile(pathValue, "utf8");
872
- return JSON.parse(raw) as AiProposalRecord;
895
+ return null;
873
896
  }
874
897
 
875
898
  function promoteTargetRef(target: string, to: "global"): string {
@@ -1122,9 +1145,13 @@ export async function draftProposal(
1122
1145
  const patchPath = patchRefForProposal(homeDir, args.rootDir, id);
1123
1146
  await mkdir(dirname(draftPath), { recursive: true });
1124
1147
  const generatedBody = renderDraftBody(current, writebacks);
1148
+ const existingDraftPath = await firstExistingFile([
1149
+ draftPath,
1150
+ ...current.draftRefs.filter((pathValue) => pathValue.endsWith(".md")),
1151
+ ]);
1125
1152
  const priorDraft =
1126
- args.append && (await fileExists(draftPath))
1127
- ? await readFile(draftPath, "utf8")
1153
+ args.append && existingDraftPath
1154
+ ? await readFile(existingDraftPath, "utf8")
1128
1155
  : null;
1129
1156
  const draftBody = args.append
1130
1157
  ? `${(priorDraft ?? generatedBody).trimEnd()}\n\n## Draft Revision\n${args.append.trim()}\n`