Skip to content

Commit 6594b0d

Browse files
sametbasbugclaude
andcommitted
Arşiv okumayı RSS'in ötesine taşı, boş panoyu reddet
İki şey Selene'nin canlı denemesinden çıktı. BOŞ PANO. `panoOku`nun boş `board` döndürdüğü bildirildi; kusur okumada değildi — o panoyu ben yazmıştım, boş olarak, "ajan yolu hâlâ çalışıyor mu" kontrolü sırasında. Ama boş bir panonun YAZILABİLMESİ gerçek bir eksikti: panonun tamamı "şu adaylar arasından seç" demek ve sıfır adaylı bir pano `selectCount: 1` ile kendi kendisiyle çelişiyor. Artık reddediliyor. ARŞİV OKUMA. `yayinlariOku` son haberleri döndürüyordu, yani RSS'in daha zayıf bir kopyasıydı: RSS gövdeyi de veriyor, o vermiyordu. Orbit'ten geçmenin bir karşılığı olmalı ve bu karşılık kısıtla değil FAZLALIKLA kurulmalı — RSS'e dokunulmadı, olduğu gibi açık. Fark artık arşivin kendisi: arama, etiket, kategori, tarih aralığı, sayfalama, ve istenirse haber gövdesi. RSS son haberleri veriyor; bu uç 580 haberin tamamını, süzülebilir halde. Aramada joker karakterler kaçırılıyor. Kaçırılmasaydı `%` gönderen biri süzgeci sessizce etkisiz hale getirirdi — arama yaptığını sanarak her şeyi alırdı. FTS5 kurmadım: 580 satırda tarama ucuz ve ayrı indeks, senkron tutulması gereken ikinci bir gerçek demek. 67 vaka; boş pano kapısı ve joker kaçırma mutasyonla sınandı. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 921b441 commit 6594b0d

4 files changed

Lines changed: 199 additions & 23 deletions

File tree

‎README.md‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,17 @@ candidate board, `haber.panoOku` reads it back (an agent that loses the
159159
`haber.yayinla` publishes one story from it, and
160160
`haber.yayindanKaldir` / `haber.yayinaAlGeri` withdraw and restore one.
161161

162-
Reading published stories does not: `haber.yayinlariOku` is open to any agent
163-
whose human has connected Haber and opened agent access. Not the open web —
164-
the request still carries an Orbit-signed document — but no publisher row.
162+
Reading the archive does not: `haber.yayinlariOku` is open to the agent of
163+
anyone who has connected Haber in Orbit and opened agent access. It is not the
164+
open web — the request still carries an Orbit-signed document — but it needs no
165+
publisher row.
166+
167+
That operation is deliberately **not** an RSS clone. The feed stays exactly as
168+
it is, public and unchanged; the Orbit path simply reaches further. RSS gives
169+
the latest stories. `haber.yayinlariOku` gives the whole archive with search,
170+
tag, category and date filters, pagination, and the article body on request.
171+
The advantage of holding an Orbit account is built by adding, not by
172+
withholding.
165173

166174
Withdrawal is deliberately narrower than publishing. It hides rather than
167175
deletes, requires a written reason, and only applies to stories published in

‎public/orbit-actions.json‎

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"properties": {
1616
"board": {
1717
"type": "array",
18-
"description": "Aday haberler. Her adayın en az id, title, url, source alanları olmalı.",
18+
"description": "Aday haberler. En az bir aday gerekiyor; boş pano reddedilir.",
1919
"items": {
2020
"type": "object",
2121
"additionalProperties": true
@@ -239,26 +239,55 @@
239239
},
240240
{
241241
"operationId": "haber.yayinlariOku",
242-
"summary": "Yayımlanmış haberleri en yeniden eskiye döndürür. Yayıncı yetkisi gerektirmez; Orbit'e bağlı her ajan çağırabilir.",
242+
"summary": "Yayımlanmış haber arşivinde arama ve süzme. Yayıncı yetkisi gerektirmez: Haber'i bağlamış ve ajan erişimini açmış her insanın ajanı çağırabilir.",
243243
"idempotent": true,
244244
"input": {
245245
"type": "object",
246246
"additionalProperties": false,
247+
"description": "Hiçbir alan zorunlu değil; boş girdi son 20 haberi döndürür.",
247248
"properties": {
248-
"limit": {
249-
"type": "integer",
250-
"minimum": 1,
251-
"maximum": 100,
252-
"description": "Kaç haber. Varsayılan 20."
249+
"arama": {
250+
"type": "string",
251+
"maxLength": 120,
252+
"description": "Başlık ve özette geçen metin."
253253
},
254254
"kategori": {
255255
"enum": [
256256
"Siyaset",
257257
"Ekonomi",
258258
"Teknoloji",
259259
"Bilim"
260-
],
261-
"description": "Tek kategoriye daraltır."
260+
]
261+
},
262+
"etiket": {
263+
"type": "string",
264+
"maxLength": 80
265+
},
266+
"tarihten": {
267+
"type": "string",
268+
"maxLength": 40,
269+
"description": "ISO tarih; bu tarihten sonrası."
270+
},
271+
"tarihe": {
272+
"type": "string",
273+
"maxLength": 40,
274+
"description": "ISO tarih; bu tarihe kadarı."
275+
},
276+
"limit": {
277+
"type": "integer",
278+
"minimum": 1,
279+
"maximum": 100,
280+
"description": "Varsayılan 20."
281+
},
282+
"offset": {
283+
"type": "integer",
284+
"minimum": 0,
285+
"maximum": 10000,
286+
"description": "Sayfalama için atlanacak kayıt."
287+
},
288+
"govde": {
289+
"type": "boolean",
290+
"description": "true ise haber metnini de döndürür (markdown)."
262291
}
263292
}
264293
},
@@ -272,8 +301,17 @@
272301
"additionalProperties": true
273302
}
274303
},
304+
"donen": {
305+
"type": "integer"
306+
},
275307
"toplam": {
276308
"type": "integer"
309+
},
310+
"offset": {
311+
"type": "integer"
312+
},
313+
"dahaVar": {
314+
"type": "boolean"
277315
}
278316
}
279317
}

‎worker/src/index.ts‎

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ export async function writeBriefAs(identity: Identity, brief: any, env: Env): Pr
186186
return json({ problems: ["brief `board` dizisi taşımıyor"] }, 400);
187187
}
188188

189+
/* BOŞ PANO KABUL EDİLMİYOR.
190+
*
191+
* Panonun tamamı "şu adaylar arasından seç" demek; sıfır adaylı bir pano
192+
* kendi kendisiyle çelişiyor ve `selectCount: 1` ile birlikte anlamsız.
193+
* Bu kapı bir karışıklıktan sonra eklendi: yerel bir denemede boş pano
194+
* yazmıştım, o pano altı saat boyunca "aktif" kaldı ve `panoOku`yu deneyen
195+
* biri boş `board` görüp kusur sandı. Kusur okumada değil, o panonun
196+
* yazılabilmiş olmasındaydı. */
197+
if (brief.board.length === 0) {
198+
return json({ problems: ["pano boş: en az bir aday gerekiyor"] }, 400);
199+
}
200+
189201
const id = crypto.randomUUID();
190202
const now = new Date();
191203
await env.DB.prepare(
@@ -445,20 +457,73 @@ export async function readBoardAs(identity: Identity, env: Env): Promise<Respons
445457
});
446458
}
447459

448-
/** Yayımlanmış haberleri döndürür. Yetki İSTEMEZ — bkz. `orbit-eylem.ts`. */
460+
/** Yayımlanmış haberleri döndürür. Yayıncı yetkisi İSTEMEZ — bkz. `orbit-eylem.ts`.
461+
*
462+
* BU UÇ RSS'İN KOPYASI DEĞİL ve olmamalı; olsaydı Orbit'ten geçmenin bir
463+
* karşılığı olmazdı. RSS son haberleri veriyor ve orada duruyor: kimseden bir
464+
* şey alınmadı. Buradan gelen fark arşivin kendisi —
465+
*
466+
* RSS → son haberler, sabit sayı, süzgeç yok, arama yok
467+
* Orbit → 580 haberin tamamı; arama, etiket, kategori, tarih aralığı,
468+
* sayfalama ve istenirse haber gövdesi
469+
*
470+
* Yani "Orbit hesabı olanın erişimi daha iyi" cümlesi bir kısıtla değil, bir
471+
* fazlalıkla kuruluyor. */
449472
export async function listPublished(input: any, env: Env): Promise<Response> {
450473
const limit = Math.min(Math.max(Number(input?.limit ?? 20) || 20, 1), 100);
451-
const kategori = typeof input?.kategori === "string" ? input.kategori : null;
474+
const offset = Math.max(Number(input?.offset ?? 0) || 0, 0);
475+
const govdeIstendi = input?.govde === true;
476+
477+
const kosullar: string[] = ["a.is_draft = 0"];
478+
const degerler: unknown[] = [];
479+
480+
if (typeof input?.kategori === "string" && input.kategori.length > 0) {
481+
kosullar.push("a.category = ?");
482+
degerler.push(input.kategori);
483+
}
484+
if (typeof input?.etiket === "string" && input.etiket.length > 0) {
485+
kosullar.push("EXISTS (SELECT 1 FROM article_tags t WHERE t.slug = a.slug AND t.tag = ?)");
486+
degerler.push(input.etiket);
487+
}
488+
if (typeof input?.tarihten === "string" && input.tarihten.length > 0) {
489+
kosullar.push("a.pub_date >= ?");
490+
degerler.push(input.tarihten);
491+
}
492+
if (typeof input?.tarihe === "string" && input.tarihe.length > 0) {
493+
kosullar.push("a.pub_date <= ?");
494+
degerler.push(input.tarihe);
495+
}
496+
497+
/* Arama başlık ve özette, LIKE ile.
498+
*
499+
* FTS5 tablosu kurmadım: 580 satırlık bir arşivde tarama zaten ucuz ve ayrı
500+
* bir indeks, senkron tutulması gereken ikinci bir gerçek demek. Kullanıcı
501+
* girdisi jokerleri KAÇIRILIYOR — kaçırılmasaydı `%` gönderen biri süzgeci
502+
* sessizce etkisiz hale getirirdi. */
503+
const arama = typeof input?.arama === "string" ? input.arama.trim() : "";
504+
if (arama.length > 0) {
505+
const kalip = `%${arama.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
506+
kosullar.push("(a.title LIKE ? ESCAPE '\\' OR a.description LIKE ? ESCAPE '\\')");
507+
degerler.push(kalip, kalip);
508+
}
509+
510+
const nerede = kosullar.join(" AND ");
511+
const sayim = await env.DB.prepare(
512+
`SELECT count(*) AS n FROM articles a WHERE ${nerede}`,
513+
).bind(...degerler).first<{ n: number }>();
514+
515+
const alanlar = govdeIstendi
516+
? "a.slug, a.title, a.description, a.category, a.author, a.pub_date, a.hero_image, a.body_md"
517+
: "a.slug, a.title, a.description, a.category, a.author, a.pub_date, a.hero_image";
452518

453519
const rows = await env.DB.prepare(
454-
`SELECT slug, title, description, category, author, pub_date, hero_image
455-
FROM articles
456-
WHERE is_draft = 0 ${kategori ? "AND category = ?" : ""}
457-
ORDER BY pub_date DESC LIMIT ?`,
458-
).bind(...(kategori ? [kategori, limit] : [limit])).all<any>();
520+
`SELECT ${alanlar} FROM articles a
521+
WHERE ${nerede} ORDER BY a.pub_date DESC LIMIT ? OFFSET ?`,
522+
).bind(...degerler, limit, offset).all<any>();
459523

524+
const sonuc = rows.results ?? [];
460525
return json({
461-
haberler: (rows.results ?? []).map((row) => ({
526+
haberler: sonuc.map((row) => ({
462527
slug: row.slug,
463528
baslik: row.title,
464529
ozet: row.description,
@@ -467,8 +532,14 @@ export async function listPublished(input: any, env: Env): Promise<Response> {
467532
yayinTarihi: row.pub_date,
468533
url: `/${row.slug}/`,
469534
gorsel: row.hero_image,
535+
...(govdeIstendi ? { govde: row.body_md } : {}),
470536
})),
471-
toplam: (rows.results ?? []).length,
537+
donen: sonuc.length,
538+
/* Süzgece uyan TOPLAM sayı; dönen sayı değil. Ajan "kaç tane var" ile
539+
"kaç tane aldım"ı ayırt edemezse sayfalamayı kuramaz. */
540+
toplam: sayim?.n ?? sonuc.length,
541+
offset,
542+
dahaVar: offset + sonuc.length < (sayim?.n ?? 0),
472543
});
473544
}
474545

‎worker/tools/orbit-eylem-tests.mjs‎

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* Orbit üzerinden ayrıca doğrulandı. Buradaki iş kapı mantığı.
1111
*/
1212
import { OPERATIONS, siteAction } from "../src/orbit-eylem.ts";
13-
import { restoreAs, withdrawAs } from "../src/index.ts";
13+
import { listPublished, restoreAs, withdrawAs } from "../src/index.ts";
1414

1515
const ISSUER = "https://orbit.example.test";
1616
const AUDIENCE = "orbit-haber";
@@ -327,6 +327,65 @@ console.log("\n── yayından kaldırma ──");
327327
}
328328
}
329329

330+
console.log("\n── arşiv okuma ──");
331+
{
332+
const sorgular = [];
333+
const arsivDB = (satirlar) => ({
334+
DB: {
335+
prepare(sql) {
336+
return {
337+
bind(...args) {
338+
sorgular.push({ sql, args });
339+
return {
340+
async first() { return { n: 42 }; },
341+
async all() { return { results: satirlar }; },
342+
async run() { return { success: true }; },
343+
};
344+
},
345+
};
346+
},
347+
},
348+
});
349+
const satir = { slug: "s1", title: "Bir haber", description: "özet", category: "Bilim",
350+
author: "Selene AI", pub_date: "2026-08-23T10:00:00Z", hero_image: null, body_md: "Gövde" };
351+
352+
{
353+
sorgular.length = 0;
354+
const r = await listPublished({}, arsivDB([satir]));
355+
const g = await r.json();
356+
check("boş girdi çalışıyor", `${g.donen} ${g.toplam} ${g.dahaVar}`, "1 42 true");
357+
check(" gövde istenmedikçe dönmüyor", g.haberler[0].govde, undefined);
358+
}
359+
{
360+
sorgular.length = 0;
361+
const g = await (await listPublished({ govde: true }, arsivDB([satir]))).json();
362+
check("govde:true gövdeyi getiriyor", g.haberler[0].govde, "Gövde");
363+
}
364+
{
365+
sorgular.length = 0;
366+
await listPublished({ arama: "iklim", kategori: "Bilim", etiket: "uzay", tarihten: "2026-01-01" }, arsivDB([]));
367+
const sorgu = sorgular.at(-1);
368+
check("süzgeçler sorguya giriyor",
369+
["a.category = ?", "article_tags", "a.pub_date >= ?", "LIKE ?"].every((p) => sorgu.sql.includes(p)), true);
370+
check(" arama değeri bağlanıyor", sorgu.args.includes("%iklim%"), true);
371+
}
372+
{
373+
/* JOKER KAÇIRMA. `%` gönderen biri süzgeci sessizce etkisiz hale
374+
* getirirdi: "hepsini getir" demek olurdu ve arama yaptığını sanırdı. */
375+
sorgular.length = 0;
376+
await listPublished({ arama: "%100_lük" }, arsivDB([]));
377+
const kalip = sorgular.at(-1).args.find((a) => typeof a === "string" && a.includes("100"));
378+
check("joker karakterler kaçırılıyor", kalip, "%\\%100\\_lük%");
379+
}
380+
{
381+
sorgular.length = 0;
382+
await listPublished({ limit: 5000, offset: -3 }, arsivDB([]));
383+
const args = sorgular.at(-1).args;
384+
check("limit tavana çekiliyor", args.at(-2), 100);
385+
check("negatif offset sıfırlanıyor", args.at(-1), 0);
386+
}
387+
}
388+
330389
console.log("\n── yetki katmanları ──");
331390
{
332391
/* Yayıncı satırı OLMAYAN bir ajan yayımlanmış haberleri okuyabilmeli:
@@ -406,7 +465,7 @@ console.log("\n── denetim izi ──");
406465
{
407466
/* Editoryal karar reddetme DEĞİL: cevaplandı, sebebi gövdede. */
408467
const db = sahteDB({ yayinciSatiri: SELENE });
409-
const r = await siteAction(istek(await belge(), { operationId: "haber.panoYaz", input: { board: "dizi değil" } }), { DB: db, ...ORTAM });
468+
const r = await siteAction(istek(await belge(), { operationId: "haber.panoYaz", input: { board: [] } }), { DB: db, ...ORTAM });
410469
const govde = await r.json();
411470
check("editoryal karar 2xx ile dönüyor", `${r.status} ${govde.output.uygulandi}`, "200 false");
412471
check(" ve denetim izine yazılmıyor", db.reddedilenler.length, 0);

0 commit comments

Comments
 (0)