@miadi/inquiry-weave 0.16.6 → 0.17.1

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.
Files changed (55) hide show
  1. package/dist/artefact-dsl.d.ts +60 -0
  2. package/dist/artefact-dsl.d.ts.map +1 -0
  3. package/dist/artefact-dsl.js +138 -0
  4. package/dist/artefact-dsl.js.map +1 -0
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +6 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/markdown-directives.d.ts +2 -0
  10. package/dist/markdown-directives.d.ts.map +1 -1
  11. package/dist/markdown-directives.js +23 -1
  12. package/dist/markdown-directives.js.map +1 -1
  13. package/dist/mcp-server.js +213 -1
  14. package/dist/mcp-server.js.map +1 -1
  15. package/dist/mint.d.ts +261 -1
  16. package/dist/mint.d.ts.map +1 -1
  17. package/dist/mint.js +565 -0
  18. package/dist/mint.js.map +1 -1
  19. package/dist/notebook.d.ts +88 -0
  20. package/dist/notebook.d.ts.map +1 -0
  21. package/dist/notebook.js +258 -0
  22. package/dist/notebook.js.map +1 -0
  23. package/dist/pde.d.ts +54 -0
  24. package/dist/pde.d.ts.map +1 -0
  25. package/dist/pde.js +165 -0
  26. package/dist/pde.js.map +1 -0
  27. package/dist/prototype.d.ts +90 -0
  28. package/dist/prototype.d.ts.map +1 -0
  29. package/dist/prototype.js +286 -0
  30. package/dist/prototype.js.map +1 -0
  31. package/dist/references.d.ts +2 -0
  32. package/dist/references.d.ts.map +1 -1
  33. package/dist/references.js +2 -0
  34. package/dist/references.js.map +1 -1
  35. package/dist/rise-dsl.d.ts +44 -0
  36. package/dist/rise-dsl.d.ts.map +1 -0
  37. package/dist/rise-dsl.js +99 -0
  38. package/dist/rise-dsl.js.map +1 -0
  39. package/dist/rise-workspaces.d.ts +34 -0
  40. package/dist/rise-workspaces.d.ts.map +1 -0
  41. package/dist/rise-workspaces.js +73 -0
  42. package/dist/rise-workspaces.js.map +1 -0
  43. package/dist/rispec.d.ts +72 -0
  44. package/dist/rispec.d.ts.map +1 -0
  45. package/dist/rispec.js +227 -0
  46. package/dist/rispec.js.map +1 -0
  47. package/dist/text-references.d.ts +3 -2
  48. package/dist/text-references.d.ts.map +1 -1
  49. package/dist/text-references.js +29 -3
  50. package/dist/text-references.js.map +1 -1
  51. package/dist/tracing.d.ts +95 -0
  52. package/dist/tracing.d.ts.map +1 -0
  53. package/dist/tracing.js +302 -0
  54. package/dist/tracing.js.map +1 -0
  55. package/package.json +2 -2
package/dist/mint.js CHANGED
@@ -32,6 +32,16 @@ import { existingReviewRelation, fetchPublicReview, projectEpisodeReview, requir
32
32
  import { readEpisodeReviews, reviewNodeId, reviewReference } from "@miadi/episodic-memory-schema";
33
33
  import { DEFAULT_FOUNDATION_RELATION, episodeFoundationPath, foundationNodeId, readEpisodeFoundations, } from "@miadi/episodic-memory-schema";
34
34
  import { existingFoundationRelation, FoundationError, foundationLookupFromDir, foundationLookupFromFront, projectEpisodeFoundation, readEpisodePacket, requireFoundationSlug, upsertEpisodeFoundation, } from "./foundation.js";
35
+ import { NotebookError, existingNotebookRelation, projectEpisodeNotebook, readNotebookFolder, requireNotebookRef, upsertEpisodeNotebook, } from "./notebook.js";
36
+ import { PrototypeError, existingPrototypeRelation, optionalEditorUrl, assertDistinctPrototypeAddresses, projectEpisodePrototype, prototypeEditorUrl, requirePrototypeSlug, requirePrototypeUrl, upsertEpisodePrototype, } from "./prototype.js";
37
+ import { TracingError, existingTracingRelation, optionalUiUrl, projectEpisodeTracing, requireTracingRef, tracingProviderName, tracingUiUrl, upsertEpisodeTracing, } from "./tracing.js";
38
+ import { PdeError, readEpisodePdeFolder, requirePdeUuid, upsertEpisodePde } from "./pde.js";
39
+ import { DEFAULT_PDE_RELATION, PDE_ENTRY_KEYS, isRelativePdePath, readEpisodePdes } from "@miadi/episodic-memory-schema";
40
+ import { RispecError, existingRispecRelation, projectEpisodeRispec, requireRiseRef, upsertEpisodeRispec } from "./rispec.js";
41
+ import { DEFAULT_RISPEC_RELATION, readEpisodeRispecs, riseNodeId, riseReference } from "@miadi/episodic-memory-schema";
42
+ import { defaultTracingRelation, readEpisodeTracing, tracingKey, tracingNodeId, } from "@miadi/episodic-memory-schema";
43
+ import { DEFAULT_PROTOTYPE_RELATION, prototypeNodeId, readEpisodePrototypes, } from "@miadi/episodic-memory-schema";
44
+ import { DEFAULT_NOTEBOOK_RELATION, notebookNodeId, readEpisodeNotebooks, } from "@miadi/episodic-memory-schema";
35
45
  import { closingIsProven, MW_RECEIPT_BASENAME, proveEpisodeClosing, refusePoisonedWheel, WHEEL_PROBE_TIMEOUT_MS, } from "./closing.js";
36
46
  export class EpisodeServiceError extends Error {
37
47
  code;
@@ -794,6 +804,70 @@ function asFoundationDoorError(error) {
794
804
  }
795
805
  throw error;
796
806
  }
807
+ function asNotebookDoorError(error) {
808
+ if (error instanceof NotebookError) {
809
+ const code = error.code === "notebook_manifest_conflict" ? "invalid_field" : error.code;
810
+ throw new EpisodeServiceError(code, error.message);
811
+ }
812
+ throw error;
813
+ }
814
+ function asPrototypeDoorError(error) {
815
+ if (error instanceof PrototypeError) {
816
+ const code = error.code === "prototype_manifest_conflict" ? "invalid_field" : error.code;
817
+ throw new EpisodeServiceError(code, error.message);
818
+ }
819
+ throw error;
820
+ }
821
+ function asTracingDoorError(error) {
822
+ if (error instanceof TracingError) {
823
+ const code = error.code === "tracing_manifest_conflict" ? "invalid_field" : error.code;
824
+ throw new EpisodeServiceError(code, error.message);
825
+ }
826
+ throw error;
827
+ }
828
+ function asRispecDoorError(error) {
829
+ if (error instanceof RispecError) {
830
+ const code = error.code === "rispec_manifest_conflict" ? "invalid_field" : error.code;
831
+ throw new EpisodeServiceError(code, error.message);
832
+ }
833
+ throw error;
834
+ }
835
+ function parseRispecRelation(raw) {
836
+ if (raw === undefined || raw === null || raw === "")
837
+ return undefined;
838
+ const relation = typeof raw === "string" ? raw.trim() : "";
839
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(relation) || relation.length > 80) {
840
+ throw new EpisodeServiceError("invalid_field", `relation '${String(raw)}' must be a kebab-case phrase completing "Episode <relation> Spec", e.g. is-specified-in or revises`);
841
+ }
842
+ return relation;
843
+ }
844
+ function parseTracingRelation(raw) {
845
+ if (raw === undefined || raw === null || raw === "")
846
+ return undefined;
847
+ const relation = typeof raw === "string" ? raw.trim() : "";
848
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(relation) || relation.length > 80) {
849
+ throw new EpisodeServiceError("invalid_field", `relation '${String(raw)}' must be a kebab-case phrase completing "Episode <relation> Resource", e.g. is-traced-in`);
850
+ }
851
+ return relation;
852
+ }
853
+ function parsePrototypeRelation(raw) {
854
+ if (raw === undefined || raw === null || raw === "")
855
+ return undefined;
856
+ const relation = typeof raw === "string" ? raw.trim() : "";
857
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(relation) || relation.length > 80) {
858
+ throw new EpisodeServiceError("invalid_field", `relation '${String(raw)}' must be a kebab-case phrase completing "Episode <relation> Prototype", e.g. is-prototyped-in`);
859
+ }
860
+ return relation;
861
+ }
862
+ function parseNotebookRelation(raw) {
863
+ if (raw === undefined || raw === null || raw === "")
864
+ return undefined;
865
+ const relation = typeof raw === "string" ? raw.trim() : "";
866
+ if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(relation) || relation.length > 80) {
867
+ throw new EpisodeServiceError("invalid_field", `relation '${String(raw)}' must be a kebab-case phrase completing "Episode <relation> Notebook", e.g. cites`);
868
+ }
869
+ return relation;
870
+ }
797
871
  function parseFoundationRelation(raw) {
798
872
  if (raw === undefined || raw === null || raw === "")
799
873
  return undefined;
@@ -904,6 +978,497 @@ export async function listEpisodeFoundations(context, ref) {
904
978
  }
905
979
  return { identity, foundations: readEpisodeFoundations(manifest) };
906
980
  }
981
+ /**
982
+ * Hold a notebook in an episode: the manifest entry (filling only what a
983
+ * hand-written entry lacks), the `notebook:<uuid>` node and the episode→notebook
984
+ * edge on the wheel, and the landing. The folder under `captures/` is read for
985
+ * its files and title when the episode keeps one; nothing is written there.
986
+ */
987
+ export async function attachEpisodeNotebook(context, ref, input) {
988
+ let notebook;
989
+ try {
990
+ notebook = requireNotebookRef(input.notebook);
991
+ }
992
+ catch (error) {
993
+ asNotebookDoorError(error);
994
+ }
995
+ const asked = parseNotebookRelation(input.relation);
996
+ const identity = await resolveDoorEpisode(context, ref);
997
+ if (!hasEpisodeManifest(identity.dir)) {
998
+ throw new EpisodeServiceError("manifest_missing", missingManifestRemedy(identity.dir));
999
+ }
1000
+ const folder = await readNotebookFolder(identity.dir, notebook.id);
1001
+ const gitRoot = await gitRootOf(context);
1002
+ return withChronicleLock(gitRoot, async () => {
1003
+ const manifestPath = join(identity.dir, "episode.yaml");
1004
+ const nodeEpisode = episodeNodeId(identity.path);
1005
+ const nodeId = notebookNodeId(notebook.id);
1006
+ let previous;
1007
+ try {
1008
+ previous = readEpisodeNotebooks(parse(await readFile(manifestPath, "utf8"))).find((entry) => entry.id === notebook.id);
1009
+ }
1010
+ catch {
1011
+ previous = undefined;
1012
+ }
1013
+ // One ordered pair is one edge on the wheel; a hand-made edge keeps its relation.
1014
+ const onWheel = await existingNotebookRelation(context.wheelUrl, nodeEpisode, nodeId, { fetchImpl: context.fetchImpl });
1015
+ const relation = onWheel ?? previous?.relation ?? asked ?? DEFAULT_NOTEBOOK_RELATION;
1016
+ const relationNote = asked && asked !== relation
1017
+ ? `asked for '${asked}', kept '${relation}': ${onWheel ? "the wheel already holds that edge" : "the episode already recorded that relation"}`
1018
+ : undefined;
1019
+ const sources = input.sources?.map((s) => s.trim()).filter(Boolean);
1020
+ const upserted = await upsertEpisodeNotebook({
1021
+ episodeYamlPath: manifestPath,
1022
+ entry: {
1023
+ id: notebook.id,
1024
+ url: notebook.url,
1025
+ ...(folder.path ? { path: folder.path } : {}),
1026
+ relation,
1027
+ ...(input.made_by?.trim() ? { made_by: input.made_by.trim() } : {}),
1028
+ ...(sources?.length ? { sources } : {}),
1029
+ ...(folder.files.length ? { files: folder.files } : {}),
1030
+ added: nowOf(context).toISOString(),
1031
+ },
1032
+ dryRun: input.dryRun,
1033
+ }).catch(asNotebookDoorError);
1034
+ const wheel = await projectEpisodeNotebook(context.wheelUrl, { episodeNodeId: nodeEpisode, episodeFolder: identity.name, entry: upserted.entry, title: folder.title, relation: upserted.entry.relation ?? relation }, { dryRun: input.dryRun, fetchImpl: context.fetchImpl });
1035
+ let landing;
1036
+ if (input.land !== false) {
1037
+ landing = await landIdentity(context, identity, { dryRun: input.dryRun }, {
1038
+ subject: `ep${identity.number ?? "?"}: notebook ${folder.title ?? notebook.id.slice(0, 8)}`,
1039
+ body: commitBody(`Notebook held through POST /api/chronicle/episodes/{ref}/notebooks.\n\nNotebook: ${notebook.url}`, []),
1040
+ });
1041
+ }
1042
+ const closing = await closingFor(context, identity.name);
1043
+ return {
1044
+ ok: closingIsProven(closing) && wheel.node.state !== "pending" && wheel.edge.state !== "pending",
1045
+ dryRun: input.dryRun === true,
1046
+ identity,
1047
+ entry: upserted.entry,
1048
+ manifest: { created: upserted.created, filled: upserted.filled, wrote: upserted.wrote },
1049
+ wheel,
1050
+ ...(relationNote ? { relationNote } : {}),
1051
+ landing,
1052
+ closing: closing.closing,
1053
+ drift: closing.drift,
1054
+ owedActions: closing.owedActions,
1055
+ };
1056
+ });
1057
+ }
1058
+ /** The notebooks an episode holds, as its manifest records them. */
1059
+ export async function listEpisodeNotebooks(context, ref) {
1060
+ const identity = await resolveDoorEpisode(context, ref);
1061
+ let manifest = null;
1062
+ try {
1063
+ manifest = parse(await readFile(join(identity.dir, "episode.yaml"), "utf8"));
1064
+ }
1065
+ catch {
1066
+ manifest = null;
1067
+ }
1068
+ return { identity, notebooks: readEpisodeNotebooks(manifest) };
1069
+ }
1070
+ /**
1071
+ * Hold a prototype in an episode: the manifest entry (published address and
1072
+ * what the episode knows), the `prototype:<slug>` node with the editing
1073
+ * address in its metadata only, the episode→prototype edge, and the landing.
1074
+ */
1075
+ export async function attachEpisodePrototype(context, ref, input) {
1076
+ let slug;
1077
+ let url;
1078
+ let editor;
1079
+ try {
1080
+ slug = requirePrototypeSlug(input.prototype);
1081
+ url = requirePrototypeUrl(input.url);
1082
+ editor = optionalEditorUrl(input.editor);
1083
+ assertDistinctPrototypeAddresses(url, editor);
1084
+ }
1085
+ catch (error) {
1086
+ asPrototypeDoorError(error);
1087
+ }
1088
+ const asked = parsePrototypeRelation(input.relation);
1089
+ const identity = await resolveDoorEpisode(context, ref);
1090
+ if (!hasEpisodeManifest(identity.dir)) {
1091
+ throw new EpisodeServiceError("manifest_missing", missingManifestRemedy(identity.dir));
1092
+ }
1093
+ const gitRoot = await gitRootOf(context);
1094
+ return withChronicleLock(gitRoot, async () => {
1095
+ const manifestPath = join(identity.dir, "episode.yaml");
1096
+ const nodeEpisode = episodeNodeId(identity.path);
1097
+ const nodeId = prototypeNodeId(slug);
1098
+ let previous;
1099
+ try {
1100
+ previous = readEpisodePrototypes(parse(await readFile(manifestPath, "utf8"))).find((entry) => entry.id === slug);
1101
+ }
1102
+ catch {
1103
+ previous = undefined;
1104
+ }
1105
+ const onWheel = await existingPrototypeRelation(context.wheelUrl, nodeEpisode, nodeId, { fetchImpl: context.fetchImpl });
1106
+ const relation = onWheel ?? previous?.relation ?? asked ?? DEFAULT_PROTOTYPE_RELATION;
1107
+ const relationNote = asked && asked !== relation
1108
+ ? `asked for '${asked}', kept '${relation}': ${onWheel ? "the wheel already holds that edge" : "the episode already recorded that relation"}`
1109
+ : undefined;
1110
+ const text = (value) => (value?.trim() ? value.trim() : undefined);
1111
+ const reads = input.reads?.map((r) => r.trim()).filter(Boolean);
1112
+ const upserted = await upsertEpisodePrototype({
1113
+ episodeYamlPath: manifestPath,
1114
+ entry: {
1115
+ id: slug,
1116
+ ...(text(input.title) ? { title: text(input.title) } : {}),
1117
+ url,
1118
+ ...(text(input.made_with) ? { made_with: text(input.made_with) } : {}),
1119
+ ...(text(input.source) ? { source: text(input.source) } : {}),
1120
+ ...(text(input.commit) ? { commit: text(input.commit) } : {}),
1121
+ ...(text(input.issue) ? { issue: text(input.issue) } : {}),
1122
+ relation,
1123
+ ...(reads?.length ? { reads } : {}),
1124
+ added: nowOf(context).toISOString(),
1125
+ },
1126
+ dryRun: input.dryRun,
1127
+ }).catch(asPrototypeDoorError);
1128
+ const wheel = await projectEpisodePrototype(context.wheelUrl, { episodeNodeId: nodeEpisode, episodeFolder: identity.name, entry: upserted.entry, editor, relation: upserted.entry.relation ?? relation }, { dryRun: input.dryRun, fetchImpl: context.fetchImpl });
1129
+ let landing;
1130
+ if (input.land !== false) {
1131
+ landing = await landIdentity(context, identity, { dryRun: input.dryRun }, {
1132
+ subject: `ep${identity.number ?? "?"}: prototype ${upserted.entry.title ?? slug}`,
1133
+ body: commitBody(`Prototype held through POST /api/chronicle/episodes/{ref}/prototypes.\n\nPrototype: miadi-prototype:${slug} (${url})`, upserted.entry.issue ? [upserted.entry.issue] : []),
1134
+ });
1135
+ }
1136
+ const closing = await closingFor(context, identity.name);
1137
+ return {
1138
+ ok: closingIsProven(closing) && wheel.node.state !== "pending" && wheel.edge.state !== "pending",
1139
+ dryRun: input.dryRun === true,
1140
+ identity,
1141
+ entry: upserted.entry,
1142
+ manifest: { created: upserted.created, filled: upserted.filled, wrote: upserted.wrote },
1143
+ wheel,
1144
+ ...(relationNote ? { relationNote } : {}),
1145
+ landing,
1146
+ closing: closing.closing,
1147
+ drift: closing.drift,
1148
+ owedActions: closing.owedActions,
1149
+ };
1150
+ });
1151
+ }
1152
+ /**
1153
+ * The prototypes an episode holds. With `includeEditor`, each one's editing
1154
+ * address is read from its wheel node; the caller has checked the viewer
1155
+ * before asking, because that address is for people who may edit the episode.
1156
+ */
1157
+ export async function listEpisodePrototypes(context, ref, options = {}) {
1158
+ const identity = await resolveDoorEpisode(context, ref);
1159
+ let manifest = null;
1160
+ try {
1161
+ manifest = parse(await readFile(join(identity.dir, "episode.yaml"), "utf8"));
1162
+ }
1163
+ catch {
1164
+ manifest = null;
1165
+ }
1166
+ const held = readEpisodePrototypes(manifest);
1167
+ if (!options.includeEditor)
1168
+ return { identity, prototypes: held };
1169
+ const prototypes = [];
1170
+ for (const entry of held) {
1171
+ const editor = await prototypeEditorUrl(context.wheelUrl, entry.id, { fetchImpl: context.fetchImpl });
1172
+ prototypes.push(editor ? { ...entry, editor } : entry);
1173
+ }
1174
+ return { identity, prototypes };
1175
+ }
1176
+ function asPdeDoorError(error) {
1177
+ if (error instanceof PdeError) {
1178
+ throw new EpisodeServiceError(error.code === "pde_manifest_conflict" ? "invalid_field" : "invalid_pde", error.message);
1179
+ }
1180
+ throw error;
1181
+ }
1182
+ const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1183
+ const REPO_NAME = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
1184
+ /**
1185
+ * Hold a PDE in an episode under `pdes:`. A PDE inside the episode's own
1186
+ * `.pde/` is held without this; declaring it adds what the folder does not say
1187
+ * (a title, an issue, the relation). A PDE kept in a repository needs its repo
1188
+ * and its path in that repository. No wheel node is written yet.
1189
+ */
1190
+ export async function attachEpisodePde(context, ref, input) {
1191
+ const text = (value) => (value?.trim() ? value.trim() : undefined);
1192
+ let uuid;
1193
+ try {
1194
+ uuid = requirePdeUuid(input.pde);
1195
+ if (text(input.repo) && !REPO_NAME.test(text(input.repo)))
1196
+ throw new PdeError("invalid_pde", `repo '${input.repo}' must be owner/repo`);
1197
+ if (text(input.repo) && !text(input.path))
1198
+ throw new PdeError("invalid_pde", "a PDE kept in a repository needs its path in that repository");
1199
+ if (text(input.path) && !isRelativePdePath(text(input.path)))
1200
+ throw new PdeError("invalid_pde", `path '${input.path}' must be relative to its repository or to the episode folder`);
1201
+ if (text(input.relation) && !KEBAB.test(text(input.relation)))
1202
+ throw new PdeError("invalid_pde", `relation '${input.relation}' must be a kebab-case phrase completing "Episode <relation> PDE"`);
1203
+ }
1204
+ catch (error) {
1205
+ asPdeDoorError(error);
1206
+ }
1207
+ const identity = await resolveDoorEpisode(context, ref);
1208
+ if (!hasEpisodeManifest(identity.dir)) {
1209
+ throw new EpisodeServiceError("manifest_missing", missingManifestRemedy(identity.dir));
1210
+ }
1211
+ const gitRoot = await gitRootOf(context);
1212
+ return withChronicleLock(gitRoot, async () => {
1213
+ const manifestPath = join(identity.dir, "episode.yaml");
1214
+ const upserted = await upsertEpisodePde({
1215
+ episodeYamlPath: manifestPath,
1216
+ entry: {
1217
+ uuid,
1218
+ ...(text(input.title) ? { title: text(input.title) } : {}),
1219
+ ...(text(input.repo) ? { repo: text(input.repo) } : {}),
1220
+ ...(text(input.path) ? { path: text(input.path).replace(/^(\.\/)+/, "").replace(/\/+$/, "") } : {}),
1221
+ ...(text(input.commit) ? { commit: text(input.commit) } : {}),
1222
+ ...(text(input.engine) ? { engine: text(input.engine) } : {}),
1223
+ ...(text(input.model) ? { model: text(input.model) } : {}),
1224
+ ...(text(input.session) ? { session: text(input.session) } : {}),
1225
+ ...(text(input.issue) ? { issue: text(input.issue) } : {}),
1226
+ relation: text(input.relation) ?? DEFAULT_PDE_RELATION,
1227
+ added: nowOf(context).toISOString(),
1228
+ },
1229
+ dryRun: input.dryRun,
1230
+ }).catch(asPdeDoorError);
1231
+ let landing;
1232
+ if (input.land !== false) {
1233
+ landing = await landIdentity(context, identity, { dryRun: input.dryRun }, {
1234
+ subject: `ep${identity.number ?? "?"}: decomposition ${upserted.entry.title ?? uuid.slice(0, 8)}`,
1235
+ body: commitBody(`Decomposition held through POST /api/chronicle/episodes/{ref}/pdes.\n\nPDE: miadi-pde:${uuid}${upserted.entry.repo ? ` (${upserted.entry.repo}:${upserted.entry.path})` : ""}`, upserted.entry.issue ? [upserted.entry.issue] : []),
1236
+ });
1237
+ }
1238
+ const closing = await closingFor(context, identity.name);
1239
+ return {
1240
+ ok: closingIsProven(closing),
1241
+ dryRun: input.dryRun === true,
1242
+ identity,
1243
+ entry: upserted.entry,
1244
+ manifest: { created: upserted.created, filled: upserted.filled, wrote: upserted.wrote },
1245
+ landing,
1246
+ closing: closing.closing,
1247
+ drift: closing.drift,
1248
+ owedActions: closing.owedActions,
1249
+ };
1250
+ });
1251
+ }
1252
+ /**
1253
+ * The PDEs an episode holds: every one in its own `.pde/` (parents before
1254
+ * children, oldest first), each enriched by what the manifest declares for the
1255
+ * same uuid, then the ones only the manifest names.
1256
+ */
1257
+ export async function listEpisodePdes(context, ref) {
1258
+ const identity = await resolveDoorEpisode(context, ref);
1259
+ let manifest = null;
1260
+ try {
1261
+ manifest = parse(await readFile(join(identity.dir, "episode.yaml"), "utf8"));
1262
+ }
1263
+ catch {
1264
+ manifest = null;
1265
+ }
1266
+ const declared = new Map(readEpisodePdes(manifest).map((entry) => [entry.uuid, entry]));
1267
+ const pdes = [];
1268
+ for (const found of await readEpisodePdeFolder(identity.dir)) {
1269
+ const listed = { ...found, held: "folder" };
1270
+ const named = declared.get(found.uuid);
1271
+ if (named) {
1272
+ declared.delete(found.uuid);
1273
+ // The folder says what was made; the manifest adds what a person said about it, and its title wins.
1274
+ const into = listed;
1275
+ for (const key of PDE_ENTRY_KEYS)
1276
+ if (named[key] !== undefined && (key === "title" || into[key] === undefined))
1277
+ into[key] = named[key];
1278
+ listed.declared = named.declared;
1279
+ }
1280
+ pdes.push(listed);
1281
+ }
1282
+ for (const named of declared.values()) {
1283
+ const { declared: where, ...entry } = named;
1284
+ pdes.push({ ...entry, held: "declared", declared: where });
1285
+ }
1286
+ return { identity, pdes };
1287
+ }
1288
+ /**
1289
+ * Hold a tracing resource in an episode: the manifest entry (domain, resource,
1290
+ * id, name, version, label, session, relation, backstage), the
1291
+ * `tracing:<domain>/<resource>/<id>` node with the provider and, when given,
1292
+ * the provider's page in its metadata, the episode→resource edge, the landing.
1293
+ */
1294
+ export async function attachEpisodeTracing(context, ref, input) {
1295
+ let target;
1296
+ let uiUrl;
1297
+ try {
1298
+ target = requireTracingRef(input);
1299
+ uiUrl = optionalUiUrl(input.ui_url);
1300
+ }
1301
+ catch (error) {
1302
+ asTracingDoorError(error);
1303
+ }
1304
+ const asked = parseTracingRelation(input.relation);
1305
+ const identity = await resolveDoorEpisode(context, ref);
1306
+ if (!hasEpisodeManifest(identity.dir)) {
1307
+ throw new EpisodeServiceError("manifest_missing", missingManifestRemedy(identity.dir));
1308
+ }
1309
+ const gitRoot = await gitRootOf(context);
1310
+ return withChronicleLock(gitRoot, async () => {
1311
+ const manifestPath = join(identity.dir, "episode.yaml");
1312
+ const nodeEpisode = episodeNodeId(identity.path);
1313
+ const nodeId = tracingNodeId(target.domain, target.resource, target.id);
1314
+ const key = tracingKey(target.domain, target.resource, target.id);
1315
+ let previous;
1316
+ try {
1317
+ previous = readEpisodeTracing(parse(await readFile(manifestPath, "utf8"))).find((entry) => tracingKey(entry.domain, entry.resource, entry.id) === key);
1318
+ }
1319
+ catch {
1320
+ previous = undefined;
1321
+ }
1322
+ const onWheel = await existingTracingRelation(context.wheelUrl, nodeEpisode, nodeId, { fetchImpl: context.fetchImpl });
1323
+ const relation = onWheel ?? previous?.relation ?? asked ?? defaultTracingRelation(target.resource);
1324
+ const relationNote = asked && asked !== relation
1325
+ ? `asked for '${asked}', kept '${relation}': ${onWheel ? "the wheel already holds that edge" : "the episode already recorded that relation"}`
1326
+ : undefined;
1327
+ const text = (value) => (value?.trim() ? value.trim() : undefined);
1328
+ const backstage = input.backstage?.map((b) => b.trim()).filter(Boolean);
1329
+ const upserted = await upsertEpisodeTracing({
1330
+ episodeYamlPath: manifestPath,
1331
+ entry: {
1332
+ domain: target.domain,
1333
+ resource: target.resource,
1334
+ id: target.id,
1335
+ ...(text(input.name) && text(input.name) !== target.id ? { name: text(input.name) } : {}),
1336
+ ...(target.version !== undefined ? { version: target.version } : {}),
1337
+ ...(text(input.label) ? { label: text(input.label) } : {}),
1338
+ ...(text(input.session) ? { session: text(input.session) } : {}),
1339
+ relation,
1340
+ ...(backstage?.length ? { backstage } : {}),
1341
+ added: nowOf(context).toISOString(),
1342
+ },
1343
+ dryRun: input.dryRun,
1344
+ }).catch(asTracingDoorError);
1345
+ const wheel = await projectEpisodeTracing(context.wheelUrl, { episodeNodeId: nodeEpisode, episodeFolder: identity.name, entry: upserted.entry, provider: tracingProviderName(), uiUrl, relation: upserted.entry.relation ?? relation }, { dryRun: input.dryRun, fetchImpl: context.fetchImpl });
1346
+ let landing;
1347
+ if (input.land !== false) {
1348
+ landing = await landIdentity(context, identity, { dryRun: input.dryRun }, {
1349
+ subject: `ep${identity.number ?? "?"}: ${target.resource} ${upserted.entry.name ?? target.id} (${target.domain})`,
1350
+ body: commitBody(`Tracing resource held through POST /api/chronicle/episodes/{ref}/tracing.\n\nResource: miadi-tracing:${key}`, []),
1351
+ });
1352
+ }
1353
+ const closing = await closingFor(context, identity.name);
1354
+ return {
1355
+ ok: closingIsProven(closing) && wheel.node.state !== "pending" && wheel.edge.state !== "pending",
1356
+ dryRun: input.dryRun === true,
1357
+ identity,
1358
+ entry: upserted.entry,
1359
+ manifest: { created: upserted.created, filled: upserted.filled, wrote: upserted.wrote },
1360
+ wheel,
1361
+ ...(relationNote ? { relationNote } : {}),
1362
+ landing,
1363
+ closing: closing.closing,
1364
+ drift: closing.drift,
1365
+ owedActions: closing.owedActions,
1366
+ };
1367
+ });
1368
+ }
1369
+ /** The tracing resources an episode holds. With `includeLinks`, each one's provider page is read from its node. */
1370
+ export async function listEpisodeTracing(context, ref, options = {}) {
1371
+ const identity = await resolveDoorEpisode(context, ref);
1372
+ let manifest = null;
1373
+ try {
1374
+ manifest = parse(await readFile(join(identity.dir, "episode.yaml"), "utf8"));
1375
+ }
1376
+ catch {
1377
+ manifest = null;
1378
+ }
1379
+ const held = readEpisodeTracing(manifest);
1380
+ const provider = tracingProviderName();
1381
+ if (!options.includeLinks)
1382
+ return { identity, tracing: held, provider };
1383
+ const tracing = [];
1384
+ for (const entry of held) {
1385
+ const ui = await tracingUiUrl(context.wheelUrl, entry, { fetchImpl: context.fetchImpl });
1386
+ tracing.push(ui ? { ...entry, ui_url: ui } : entry);
1387
+ }
1388
+ return { identity, tracing, provider };
1389
+ }
1390
+ /** Hold a RISE specification in an episode: the manifest entry, the `rise:` node, the edge, the landing. */
1391
+ export async function attachEpisodeRispec(context, ref, input) {
1392
+ let spec;
1393
+ try {
1394
+ spec = requireRiseRef(input.spec);
1395
+ }
1396
+ catch (error) {
1397
+ asRispecDoorError(error);
1398
+ }
1399
+ const asked = parseRispecRelation(input.relation);
1400
+ const identity = await resolveDoorEpisode(context, ref);
1401
+ if (!hasEpisodeManifest(identity.dir)) {
1402
+ throw new EpisodeServiceError("manifest_missing", missingManifestRemedy(identity.dir));
1403
+ }
1404
+ const gitRoot = await gitRootOf(context);
1405
+ return withChronicleLock(gitRoot, async () => {
1406
+ const manifestPath = join(identity.dir, "episode.yaml");
1407
+ const nodeEpisode = episodeNodeId(identity.path);
1408
+ const nodeId = riseNodeId(spec);
1409
+ const address = riseReference(spec.owner, spec.repo, spec.path);
1410
+ let previous;
1411
+ try {
1412
+ previous = readEpisodeRispecs(parse(await readFile(manifestPath, "utf8"))).find((entry) => entry.ref === address);
1413
+ }
1414
+ catch {
1415
+ previous = undefined;
1416
+ }
1417
+ const onWheel = await existingRispecRelation(context.wheelUrl, nodeEpisode, nodeId, { fetchImpl: context.fetchImpl });
1418
+ const relation = onWheel ?? previous?.relation ?? asked ?? DEFAULT_RISPEC_RELATION;
1419
+ const relationNote = asked && asked !== relation
1420
+ ? `asked for '${asked}', kept '${relation}': ${onWheel ? "the wheel already holds that edge" : "the episode already recorded that relation"}`
1421
+ : undefined;
1422
+ const text = (value) => (value?.trim() ? value.trim() : undefined);
1423
+ const upserted = await upsertEpisodeRispec({
1424
+ episodeYamlPath: manifestPath,
1425
+ entry: {
1426
+ ref: address,
1427
+ ...(text(input.title) ? { title: text(input.title) } : {}),
1428
+ ...(text(input.commit) ?? spec.commit ? { commit: text(input.commit) ?? spec.commit } : {}),
1429
+ relation,
1430
+ ...(text(input.derived_from) ? { derived_from: text(input.derived_from) } : {}),
1431
+ ...(text(input.system) ? { system: text(input.system) } : {}),
1432
+ added: nowOf(context).toISOString(),
1433
+ },
1434
+ dryRun: input.dryRun,
1435
+ }).catch(asRispecDoorError);
1436
+ const wheel = await projectEpisodeRispec(context.wheelUrl, { episodeNodeId: nodeEpisode, episodeFolder: identity.name, ref: spec, entry: upserted.entry, relation: upserted.entry.relation ?? relation }, { dryRun: input.dryRun, fetchImpl: context.fetchImpl });
1437
+ let landing;
1438
+ if (input.land !== false) {
1439
+ landing = await landIdentity(context, identity, { dryRun: input.dryRun }, {
1440
+ subject: `ep${identity.number ?? "?"}: spec ${upserted.entry.title ?? spec.path}`,
1441
+ body: commitBody(`Specification held through POST /api/chronicle/episodes/{ref}/rispecs.\n\nSpec: ${address}${upserted.entry.commit ? `@${upserted.entry.commit}` : ""}`, []),
1442
+ });
1443
+ }
1444
+ const closing = await closingFor(context, identity.name);
1445
+ return {
1446
+ ok: closingIsProven(closing) && wheel.node.state !== "pending" && wheel.edge.state !== "pending",
1447
+ dryRun: input.dryRun === true,
1448
+ identity,
1449
+ entry: upserted.entry,
1450
+ manifest: { created: upserted.created, filled: upserted.filled, wrote: upserted.wrote },
1451
+ wheel,
1452
+ ...(relationNote ? { relationNote } : {}),
1453
+ landing,
1454
+ closing: closing.closing,
1455
+ drift: closing.drift,
1456
+ owedActions: closing.owedActions,
1457
+ };
1458
+ });
1459
+ }
1460
+ /** The specs an episode holds, as its manifest records them. */
1461
+ export async function listEpisodeRispecs(context, ref) {
1462
+ const identity = await resolveDoorEpisode(context, ref);
1463
+ let manifest = null;
1464
+ try {
1465
+ manifest = parse(await readFile(join(identity.dir, "episode.yaml"), "utf8"));
1466
+ }
1467
+ catch {
1468
+ manifest = null;
1469
+ }
1470
+ return { identity, rispecs: readEpisodeRispecs(manifest) };
1471
+ }
907
1472
  /** The screenwalks an episode holds (`captures/screenwalk-<id>/`), for the door's GET. */
908
1473
  export async function listEpisodeScreenwalks(context, ref) {
909
1474
  const identity = await resolveDoorEpisode(context, ref);