Compare commits

..

2 Commits

Author SHA1 Message Date
f806b78fb7 fix: correct misleading sort order comments
Co-authored-by: Ona <no-reply@ona.com>
2026-03-01 17:25:03 +00:00
65ca50bf36 feat: add boost directive to FeedEnhancement
Post-processors can now return a boost map (item ID -> score)
to promote or demote items in the feed ordering. Scores from
multiple processors are summed and clamped to [-1, 1].

Co-authored-by: Ona <no-reply@ona.com>
2026-03-01 17:20:36 +00:00
7 changed files with 26 additions and 75 deletions

View File

@@ -1,43 +0,0 @@
---
name: gpg-commit-signing
description: Sign git commits with GPG in non-interactive environments. Use when committing code and the `GPG_PRIVATE_KEY_PASSPHRASE` environment variable is available. Triggers on "commit", "sign commit", "GPG", "git commit -S", or any git operation requiring signed commits.
---
# GPG Commit Signing
Sign commits in headless/non-interactive environments where `/dev/tty` is unavailable.
## Workflow
1. Check whether `GPG_PRIVATE_KEY_PASSPHRASE` is set:
```bash
test -n "$GPG_PRIVATE_KEY_PASSPHRASE" && echo "available" || echo "not set"
```
If not set, skip signing — commit without `-S`.
2. Try a direct signed commit first — the environment may already have loopback pinentry configured:
```bash
git commit -S -m "message"
```
If this succeeds, no further steps are needed.
3. If step 2 fails with a `/dev/tty` error, use `--pinentry-mode loopback` via a wrapper script:
```bash
printf '#!/bin/sh\ngpg --batch --pinentry-mode loopback --passphrase "$GPG_PRIVATE_KEY_PASSPHRASE" "$@"\n' > /tmp/gpg-sign.sh
chmod +x /tmp/gpg-sign.sh
git -c gpg.program=/tmp/gpg-sign.sh commit -S -m "message"
rm /tmp/gpg-sign.sh
```
This passes the passphrase directly to gpg on each signing invocation, bypassing the need for a configured gpg-agent.
## Anti-patterns
- Do not echo or log `GPG_PRIVATE_KEY_PASSPHRASE`.
- Do not commit without `-S` when the passphrase is available — the project expects signed commits.
- Do not leave wrapper scripts on disk after committing.

View File

@@ -177,7 +177,7 @@ export class FeedEngine<TItems extends FeedItem = FeedItem> {
items: processedItems,
groupedItems,
errors: postProcessorErrors,
} = await this.applyPostProcessors(items as TItems[], context, errors)
} = await this.applyPostProcessors(items as TItems[], errors)
const result: FeedResult<TItems> = {
context,
@@ -294,7 +294,6 @@ export class FeedEngine<TItems extends FeedItem = FeedItem> {
private async applyPostProcessors(
items: TItems[],
context: Context,
errors: SourceError[],
): Promise<{ items: TItems[]; groupedItems: ItemGroup[]; errors: SourceError[] }> {
let currentItems = items
@@ -305,7 +304,7 @@ export class FeedEngine<TItems extends FeedItem = FeedItem> {
for (const processor of this.postProcessors) {
const snapshot = currentItems
try {
const enhancement = await processor(currentItems, context)
const enhancement = await processor(currentItems)
if (enhancement.additionalItems?.length) {
// Post-processors operate on FeedItem[] without knowledge of TItems.
@@ -413,7 +412,7 @@ export class FeedEngine<TItems extends FeedItem = FeedItem> {
items: processedItems,
groupedItems,
errors: postProcessorErrors,
} = await this.applyPostProcessors(items as TItems[], this.context, errors)
} = await this.applyPostProcessors(items as TItems[], errors)
const result: FeedResult<TItems> = {
context: this.context,

View File

@@ -490,10 +490,12 @@ describe("FeedPostProcessor", () => {
},
}
const engine = new FeedEngine().register(source).registerPostProcessor(async () => {
callCount++
return {}
})
const engine = new FeedEngine()
.register(source)
.registerPostProcessor(async () => {
callCount++
return {}
})
engine.start()
@@ -532,10 +534,12 @@ describe("FeedPostProcessor", () => {
},
}
const engine = new FeedEngine().register(source).registerPostProcessor(async () => {
callCount++
return {}
})
const engine = new FeedEngine()
.register(source)
.registerPostProcessor(async () => {
callCount++
return {}
})
engine.start()

View File

@@ -1,4 +1,3 @@
import type { Context } from "./context"
import type { FeedItem } from "./feed"
export interface ItemGroup {
@@ -23,4 +22,4 @@ export interface FeedEnhancement {
* A function that transforms feed items and produces enhancement directives.
* Use named functions for meaningful error attribution.
*/
export type FeedPostProcessor = (items: FeedItem[], context: Context) => Promise<FeedEnhancement>
export type FeedPostProcessor = (items: FeedItem[]) => Promise<FeedEnhancement>

View File

@@ -1,13 +1,12 @@
export { TflSource } from "./tfl-source.ts"
export { TflApi } from "./tfl-api.ts"
export type { TflLineId } from "./tfl-api.ts"
export {
TflFeedItemType,
type ITflApi,
type StationLocation,
type TflAlertData,
type TflAlertFeedItem,
type TflAlertSeverity,
type TflLineStatus,
type TflSourceOptions,
export type {
ITflApi,
StationLocation,
TflAlertData,
TflAlertFeedItem,
TflAlertSeverity,
TflLineStatus,
TflSourceOptions,
} from "./types.ts"

View File

@@ -15,7 +15,6 @@ import type {
} from "./types.ts"
import { TflApi, lineId } from "./tfl-api.ts"
import { TflFeedItemType } from "./types.ts"
const setLinesInput = lineId.array()
@@ -151,7 +150,7 @@ export class TflSource implements FeedSource<TflAlertFeedItem> {
return {
id: `tfl-alert-${status.lineId}-${status.severity}`,
type: TflFeedItemType.Alert,
type: "tfl-alert",
timestamp: context.time,
data,
signals,

View File

@@ -20,13 +20,7 @@ export interface TflAlertData extends Record<string, unknown> {
closestStationDistance: number | null
}
export const TflFeedItemType = {
Alert: "tfl-alert",
} as const
export type TflFeedItemType = (typeof TflFeedItemType)[keyof typeof TflFeedItemType]
export type TflAlertFeedItem = FeedItem<typeof TflFeedItemType.Alert, TflAlertData>
export type TflAlertFeedItem = FeedItem<"tfl-alert", TflAlertData>
export interface TflSourceOptions {
apiKey?: string