292 lines
9.8 KiB
TypeScript
292 lines
9.8 KiB
TypeScript
/** UI向けのHTTP代替データ。Journalの正本・認可・永続化の実装ではない。 */
|
|
export const journalFixture = {
|
|
workspaceId: "memory-review",
|
|
subjectId:
|
|
"release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review",
|
|
memoryId:
|
|
"memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001",
|
|
candidateId: "candidate-release-decision-0001",
|
|
literalQuery: "release [a+b] %_",
|
|
};
|
|
|
|
function journal(id: string, subjectId: string) {
|
|
return {
|
|
id,
|
|
subject_id: subjectId,
|
|
source_session_id: `session-${id}`,
|
|
title: id === "journal-release"
|
|
? "Release coordination Session"
|
|
: id === "journal-review"
|
|
? "Independent review Session"
|
|
: "Archived planning Session",
|
|
current_position_bytes: 120,
|
|
process_order: 24,
|
|
process_byte: 120,
|
|
state_token: `state-${id}`,
|
|
created_at: "2026-01-02T03:04:05Z",
|
|
updated_at: "2026-01-04T03:04:05Z",
|
|
};
|
|
}
|
|
function source(journalId: string) {
|
|
return journalId === "journal-release"
|
|
? { state: "missing", history_href: null }
|
|
: journalId === "journal-review"
|
|
? { state: "unavailable", history_href: null }
|
|
: {
|
|
state: "unavailable",
|
|
history_href: "/w/memory-review/workers/W-fixture/console",
|
|
};
|
|
}
|
|
function entry(index: number) {
|
|
const journalId = index === 4
|
|
? "journal-planning"
|
|
: index % 2
|
|
? "journal-review"
|
|
: "journal-release";
|
|
return {
|
|
id: `entry-${String(index).padStart(2, "0")}`,
|
|
journal_id: journalId,
|
|
position: index + 1,
|
|
state: index === 0 ? "open" : "fixed",
|
|
title: index === 2
|
|
? "Corrected release evidence"
|
|
: `Release milestone ${
|
|
String(index).padStart(2, "0")
|
|
} · ${journalFixture.literalQuery}`,
|
|
correction_refs: index === 2 ? ["entry-00"] : [],
|
|
content_token: `content-entry-${index}`,
|
|
created_at: "2026-01-02T03:04:05Z",
|
|
updated_at: "2026-01-03T03:04:05Z",
|
|
fixed_at: index === 0 ? null : "2026-01-03T03:04:05Z",
|
|
};
|
|
}
|
|
const entries = Array.from({ length: 28 }, (_, index) => entry(index));
|
|
const related = Array.from({ length: 8 }, (_, index) => ({
|
|
related: {
|
|
selector: {
|
|
memory_id: index === 0
|
|
? journalFixture.memoryId
|
|
: `memory-related-${index}`,
|
|
change_id: "change-current",
|
|
candidate_id: journalFixture.candidateId,
|
|
},
|
|
source_index: index,
|
|
reconsideration: index === 0,
|
|
},
|
|
claim: index === 0
|
|
? "Keep subject Memory provenance typed and visible"
|
|
: `Related release lesson ${index}`,
|
|
}));
|
|
const body =
|
|
`# Preserved Journal narrative\n\nThis saved Journal remains readable without the original Worker or Session. It is not the original transcript or proof of each Memory claim.\n\n${
|
|
Array.from({ length: 64 }, (_, i) =>
|
|
`## Release event ${
|
|
i + 1
|
|
}\n\nThe team compared the available evidence and recorded the release decision. Literal search token: ${journalFixture.literalQuery}.`)
|
|
.join("\n\n")
|
|
}\n\n## Journal narrative complete`;
|
|
|
|
const json = (value: unknown, status = 200) =>
|
|
new Response(JSON.stringify(value), {
|
|
status,
|
|
headers: { "content-type": "application/json; charset=utf-8" },
|
|
});
|
|
const failure = (status: number, message: string) =>
|
|
json(
|
|
{ error: status === 403 ? "Forbidden" : "Unavailable", message },
|
|
status,
|
|
);
|
|
|
|
export async function journalBrowserFixture(
|
|
request: Request,
|
|
subjectId: string,
|
|
): Promise<Response> {
|
|
if (request.method !== "POST") {
|
|
return failure(405, "read-only Journal queries require POST");
|
|
}
|
|
const r = await request.json().catch(() => null) as
|
|
| Record<string, any>
|
|
| null;
|
|
if (!r || typeof r.action !== "string") {
|
|
return failure(400, "missing query action");
|
|
}
|
|
const reply = (value: unknown) => json({ kind: r.action, value });
|
|
if (subjectId === "failed-subject") {
|
|
return failure(503, "Journal read temporarily unavailable");
|
|
}
|
|
if (r.journal_id === "journal-denied") {
|
|
return failure(403, "Journal read permission denied");
|
|
}
|
|
const empty = subjectId === "empty-subject" ||
|
|
subjectId === "ungenerated-subject";
|
|
switch (r.action) {
|
|
case "sessions": {
|
|
if (empty) return reply({ items: [], next: null });
|
|
const journals = ["journal-release", "journal-review", "journal-archive"];
|
|
const offset = r.after === "fixture-sessions-2" ? 2 : 0;
|
|
return reply({
|
|
items: journals.slice(offset, offset + 2).map((id) => ({
|
|
journal: journal(id, subjectId),
|
|
original_source: source(id),
|
|
unreflected_sources: id === "journal-review",
|
|
})),
|
|
next: offset === 0 ? "fixture-sessions-2" : null,
|
|
});
|
|
}
|
|
case "entries": {
|
|
if (r.query === "fixture-denied") {
|
|
return failure(403, "Journal read permission denied");
|
|
}
|
|
if (r.query === "fixture-failure") {
|
|
return failure(503, "Journal list unavailable");
|
|
}
|
|
const filtered = empty
|
|
? []
|
|
: entries.filter((e) =>
|
|
(!r.journal_id || r.journal_id === e.journal_id) &&
|
|
(!r.query ||
|
|
`${e.title} ${body}`.toLowerCase().includes(
|
|
String(r.query).toLowerCase(),
|
|
))
|
|
);
|
|
const offset = r.after === "fixture-entries-2" ? 16 : 0;
|
|
const limit = Math.min(Number(r.limit) || 20, 16);
|
|
return reply({
|
|
items: filtered.slice(offset, offset + limit),
|
|
next: offset + limit < filtered.length ? "fixture-entries-2" : null,
|
|
});
|
|
}
|
|
case "entry": {
|
|
const e = entries.find((e) =>
|
|
e.id === r.entry_id && e.journal_id === r.journal_id
|
|
);
|
|
return e
|
|
? reply({
|
|
journal: journal(e.journal_id, subjectId),
|
|
entry: e,
|
|
correction_targets: entries.filter((target) =>
|
|
e.correction_refs.includes(target.id)
|
|
),
|
|
original_source: source(e.journal_id),
|
|
})
|
|
: failure(404, "Journal entry not found");
|
|
}
|
|
case "text": {
|
|
const e = r.entry_id
|
|
? entries.find((e) =>
|
|
e.id === r.entry_id && e.journal_id === r.journal_id
|
|
)
|
|
: null;
|
|
const token = r.field === "current_position"
|
|
? `state-${r.journal_id}`
|
|
: e?.content_token;
|
|
if (!token) return failure(404, "Journal text not found");
|
|
if (r.expected_token && r.expected_token !== token) {
|
|
return failure(409, "Journal text changed; reread from the beginning");
|
|
}
|
|
const text = r.field === "body"
|
|
? body
|
|
: r.field === "ongoing"
|
|
? "## Ongoing release context\n\nThe next review is still open."
|
|
: "## Current Session position\n\nRelease review is awaiting the next evidence check. This current position does not replace the event history.";
|
|
const bytes = new TextEncoder().encode(text);
|
|
const start = Number(r.byte_offset) || 0;
|
|
let end = Math.min(
|
|
bytes.length,
|
|
start + Math.min(Number(r.max_bytes) || 16384, 16384),
|
|
);
|
|
while (end < bytes.length && (bytes[end] & 0xc0) === 0x80) end -= 1;
|
|
return reply({
|
|
text: new TextDecoder().decode(bytes.slice(start, end)),
|
|
next_byte: end < bytes.length ? end : null,
|
|
content_token: token,
|
|
});
|
|
}
|
|
case "corrections":
|
|
return reply({
|
|
items: r.entry_id === "entry-00" ? [entry(2)] : [],
|
|
next: null,
|
|
});
|
|
case "related_memory": {
|
|
if (r.entry_id === "entry-03") return reply({ items: [], next: null });
|
|
const offset = r.after ? Number(r.after.source_index) + 1 : 0;
|
|
const limit = Math.min(Number(r.limit) || 3, 20);
|
|
const items = related.slice(offset, offset + limit);
|
|
const last = items.at(-1)?.related;
|
|
return reply({
|
|
items,
|
|
next: offset + limit < related.length && last
|
|
? { ...last.selector, source_index: last.source_index }
|
|
: null,
|
|
});
|
|
}
|
|
case "memory_sources": {
|
|
const s = r.selector;
|
|
// 不変selectorは本番Memory loaderから受け取り、routeだけから推測しない。
|
|
if (
|
|
!s || s.candidate_id !== journalFixture.candidateId ||
|
|
![
|
|
"change-current",
|
|
"change-earlier",
|
|
"change-original",
|
|
"change-parent",
|
|
].includes(
|
|
s.change_id,
|
|
)
|
|
) return failure(404, "Immutable candidate not found");
|
|
if (s.memory_id === "memory-sources-failure") {
|
|
return failure(503, "Related Journal unavailable");
|
|
}
|
|
if (s.memory_id === "memory-resolved-0002") {
|
|
return reply({
|
|
selector: s,
|
|
sources: [{
|
|
link: {
|
|
source_index: 0,
|
|
original_source: "unknown",
|
|
connection: {
|
|
state: "unconnected",
|
|
reason: "insufficient_source",
|
|
},
|
|
},
|
|
entry: null,
|
|
original_source: { state: "unavailable", history_href: null },
|
|
}],
|
|
total_sources: 1,
|
|
next_source_index: null,
|
|
});
|
|
}
|
|
const offset = Number(r.source_offset) || 0;
|
|
const limit = Math.min(Number(r.limit) || 3, 20);
|
|
const sources = entries.slice(offset, Math.min(8, offset + limit)).map((
|
|
e,
|
|
index,
|
|
) => ({
|
|
link: {
|
|
source_index: offset + index,
|
|
original_source: e.journal_id === "journal-release"
|
|
? "missing"
|
|
: "unknown",
|
|
connection: {
|
|
state: "linked",
|
|
journal_id: e.journal_id,
|
|
entry_id: e.id,
|
|
target: "available",
|
|
reconsideration: e.id === "entry-00",
|
|
},
|
|
},
|
|
entry: e,
|
|
original_source: source(e.journal_id),
|
|
}));
|
|
return reply({
|
|
selector: s,
|
|
sources,
|
|
total_sources: 8,
|
|
next_source_index: offset + limit < 8 ? offset + limit : null,
|
|
});
|
|
}
|
|
default:
|
|
return failure(400, "unsupported read-only Journal query");
|
|
}
|
|
}
|