Aggregation consolidation: stable names, membership eligibility, nightly re-cluster #79

Manually merged
marvin8 merged 12 commits from feat/issue-71-aggregation-consolidation into main 2026-10-11 00:18:34 +00:00
Collaborator

Closes #71

Ongoing stories stop spawning a new aggregate every day. Four linked changes, in dependency order:

  1. Stable identity — aggregates gains a name column (alembic a88dd3534780). The migration backfills name from each summary's first line (trimmed) and rewrites summary to the text after it, so pre-existing rows stop printing the title twice now that the reader titles from the column. The aggregator writes name and overview separately; updates rewrite only the overview, so a group keeps its name. The reader titles groups from the column (Untitled group for blank legacy names).
  2. Prompt anchors — EXISTING TOPICS now anchors each group with its stable name plus up to five recent member titles instead of the drifting summary, and the system prompt requires every new article to land in exactly one topic. A partial assignment logs a WARNING naming the omitted ids; it never rejects the grouping.
  3. Membership-based eligibility — the max(updated_at) watermark is gone. Eligible = subscribed-feed article with no aggregate membership and created_at within YUNJIN_ARTICLE_ELIGIBILITY_DAYS (default 30, inclusive). Stranded articles are retried every run, and TTL expiry releases articles for free because the cascade delete drops the membership.
  4. Nightly consolidation — when due (YUNJIN_CONSOLIDATION_INTERVAL_HOURS, default 24, tracked per user in last_consolidation_at) the trailing YUNJIN_CONSOLIDATION_WINDOW_HOURS (default 72) plus all eligible articles are re-clustered against the aggregates they touch, instead of the incremental call. The LLM call runs first: API or parse failure mutates nothing and does not stamp the timestamp, so the next due run retries. On success the pass unlinks only the current user's memberships (a shared article may sit in another user's group), applies the topics, deletes emptied aggregates, and writes the timestamp.

Documentation: the three new YUNJIN_* variables are in docs/config.md, and the worker-pipeline description notes the periodic consolidation stage.

Verification: full uv run nox green (ty, ruff, complexipy, pysentry, pytest on 3.12/3.13/3.14, deptry, zaojun); 679 tests pass. One fresh-context review pass; all findings addressed.

Closes #71 Ongoing stories stop spawning a new aggregate every day. Four linked changes, in dependency order: 1. **Stable identity** — `aggregates` gains a `name` column (alembic `a88dd3534780`). The migration backfills `name` from each summary's first line (trimmed) and rewrites `summary` to the text after it, so pre-existing rows stop printing the title twice now that the reader titles from the column. The aggregator writes name and overview separately; updates rewrite only the overview, so a group keeps its name. The reader titles groups from the column (`Untitled group` for blank legacy names). 2. **Prompt anchors** — EXISTING TOPICS now anchors each group with its stable name plus up to five recent member titles instead of the drifting summary, and the system prompt requires every new article to land in exactly one topic. A partial assignment logs a WARNING naming the omitted ids; it never rejects the grouping. 3. **Membership-based eligibility** — the `max(updated_at)` watermark is gone. Eligible = subscribed-feed article with no aggregate membership and `created_at` within `YUNJIN_ARTICLE_ELIGIBILITY_DAYS` (default 30, inclusive). Stranded articles are retried every run, and TTL expiry releases articles for free because the cascade delete drops the membership. 4. **Nightly consolidation** — when due (`YUNJIN_CONSOLIDATION_INTERVAL_HOURS`, default 24, tracked per user in `last_consolidation_at`) the trailing `YUNJIN_CONSOLIDATION_WINDOW_HOURS` (default 72) plus all eligible articles are re-clustered against the aggregates they touch, instead of the incremental call. The LLM call runs first: API or parse failure mutates nothing and does not stamp the timestamp, so the next due run retries. On success the pass unlinks only the current user's memberships (a shared article may sit in another user's group), applies the topics, deletes emptied aggregates, and writes the timestamp. Documentation: the three new `YUNJIN_*` variables are in `docs/config.md`, and the worker-pipeline description notes the periodic consolidation stage. Verification: full `uv run nox` green (ty, ruff, complexipy, pysentry, pytest on 3.12/3.13/3.14, deptry, zaojun); 679 tests pass. One fresh-context review pass; all findings addressed.
Issue #71 task 1: aggregates gains a stable name column, backfilled
from each summary's first line. create_aggregate takes name before
summary; update_aggregate stays summary-only so creation-time names
never change. Current call sites bridge with name = first line of the
summary they write (replaced by topic.name in task 2).
Issue #71 task 2: created aggregates store topic name and overview
separately — the 'name\n\nsummary' blob is gone. Updates rewrite only
the summary, so a group's name stays stable across aggregation runs.
Issue #71 task 3: index cards and the aggregate page take their title
from aggregates.name (placeholder 'Untitled group' for blank legacy
names); _split_aggregate_summary and its blob parsing are deleted.
Issue #71 task 4: EXISTING TOPICS now anchors each group with its
stable name and up to five recent member titles — never the summary
itself. The prompt also demands every new article land in exactly one
topic, so the LLM cannot silently drop articles.
Issue #71 task 5: the max(updated_at) watermark is gone. An article is
eligible when its feed is subscribed, it belongs to none of the user's
aggregates, and it was created within YUNJIN_ARTICLE_ELIGIBILITY_DAYS
(default 30, inclusive). TTL expiry releases articles automatically:
deleting the aggregate deletes the membership.
Issue #71 task 6: a partial assignment logs a WARNING naming the
omitted article ids. Never raises or retries — the omitted articles
simply stay eligible for the next run.
Issue #71 task 7: when a user's pass is due (YUNJIN_CONSOLIDATION_
INTERVAL_HOURS, default 24) the trailing YUNJIN_CONSOLIDATION_WINDOW_
HOURS (default 72) plus all eligible articles are re-clustered against
the aggregates they touch. The LLM call runs first — failure or parse
error mutates nothing and writes no timestamp, so the next due run
retries. On success: unlink, re-apply, delete emptied aggregates, stamp
last_consolidation_at.
Issue #71 task 8: documents YUNJIN_ARTICLE_ELIGIBILITY_DAYS,
YUNJIN_CONSOLIDATION_INTERVAL_HOURS, and YUNJIN_CONSOLIDATION_WINDOW_
HOURS in docs/config.md, and notes the periodic consolidation stage in
the worker-pipeline description.
🐛 Fix review findings: user-scoped unlink, migration backfill, consolidation timing
All checks were successful
/ gitleaks (pull_request) Successful in 33s
/ pr-review (pull_request) Successful in 1m26s
/ checks (pull_request) Successful in 31m31s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m48s
18ae8f6869
Final review pass findings:

- Critical: consolidation unlinked articles across ALL users'
  aggregates. clear_article_aggregate_links is now scoped to the
  consolidating user, so a shared article's membership in another
  user's group survives (one article may legitimately sit in several
  users' aggregates).
- Important: pre-migration summaries kept the whole 'name\n\noverview'
  blob while the reader now titles from the name column, so legacy rows
  printed the title twice. The migration rewrites summary to the text
  after the first line, trimmed (spec: 'summary becomes overview-only',
  'backfill keeps old data rendering correctly').
- Important: a due consolidation ran alongside the incremental call.
  Per the spec it now runs instead of it, so an article the pass leaves
  unassigned waits for the next hourly run instead of triggering a
  second LLM call in the same pass.
forgejo-actions left a comment

WuMing

Found 8 issue(s). See inline comments below.

## WuMing Found **8** issue(s). See inline comments below.
docs/config.md Outdated
@ -13,6 +13,9 @@ Aggregation needs access to an **OpenAI-compatible chat completions API**
| `YUNJIN_LLM_API_KEY` | *none* — **required** for aggregation | API key for the chat completions endpoint. Aggregation fails fast with a clear error when it is unset. |
| `YUNJIN_LLM_BASE_URL` | `https://openrouter.ai/api/v1` | Base URL of the OpenAI-compatible API. |
| `YUNJIN_LLM_MODEL` | `deepseek/deepseek-v4.1-flash` | Model used to group articles and write aggregate titles/overviews. |
| `YUNJIN_ARTICLE_ELIGIBILITY_DAYS` | `30` | Age floor (days) for aggregation eligibility: an article is only sent to the LLM if it was created within this window and belongs to none of the user's aggregates. Invalid or below-1 values fall back to the default. |

docs [MEDIUM]

"Age floor" is inaccurate and will mislead readers: a floor implies a minimum age, but the variable is a maximum age/recency window — articles whose created_at is older than YUNJIN_ARTICLE_ELIGIBILITY_DAYS are excluded from the LLM call. Reword to something like "Maximum age (days) for aggregation eligibility" so the direction of the bound is unambiguous.

**docs** [MEDIUM] "Age floor" is inaccurate and will mislead readers: a floor implies a minimum age, but the variable is a *maximum* age/recency window — articles whose `created_at` is older than `YUNJIN_ARTICLE_ELIGIBILITY_DAYS` are excluded from the LLM call. Reword to something like "Maximum age (days) for aggregation eligibility" so the direction of the bound is unambiguous. <!-- wuming:sha256:315f072fef58585a93d690b0ed19031514636a253c899e48f838bdeab9113380 -->

docs [LOW]

The eligibility conditions are incomplete. Per the implementation, an article is eligible only if it comes from a subscribed feed, has no aggregate membership, and its created_at falls within the window — and the boundary is inclusive. The sentence omits the subscribed-feed requirement and the inclusive boundary, so operators cannot tell whether a borderline 30-day-old article from an unsubscribed feed is sent. Add both conditions, e.g. "…only sent to the LLM if it belongs to a subscribed feed, is not already in one of the user's aggregates, and was created within this many days (inclusive)."

**docs** [LOW] The eligibility conditions are incomplete. Per the implementation, an article is eligible only if it comes from a subscribed feed, has no aggregate membership, and its `created_at` falls within the window — and the boundary is inclusive. The sentence omits the subscribed-feed requirement and the inclusive boundary, so operators cannot tell whether a borderline 30-day-old article from an unsubscribed feed is sent. Add both conditions, e.g. "…only sent to the LLM if it belongs to a subscribed feed, is not already in one of the user's aggregates, and was created within this many days (inclusive)." <!-- wuming:sha256:315f072fef58585a93d690b0ed19031514636a253c899e48f838bdeab9113380 -->
@ -298,2 +309,2 @@
agg = agg_db.create_aggregate(db_conn, user_id=1, summary="Existing Topic\n\nAlready grouped.")
for article in [a1, a2]:
"""When every article is already a member of an aggregate, no LLM call is made."""
self._mark_consolidation_done(db_conn)

tests [LOW]

self._mark_consolidation_done(db_conn) was inserted as the first statement, so the following string literal is no longer the function's docstring (it becomes a dead expression statement). Move the call below the docstring to preserve the test's documentation.

**tests** [LOW] `self._mark_consolidation_done(db_conn)` was inserted as the first statement, so the following string literal is no longer the function's docstring (it becomes a dead expression statement). Move the call below the docstring to preserve the test's documentation. <!-- wuming:sha256:69a4d60b2c206108e7158e3820319d446ed13629267d7d24467e3c874dcfa81f -->

tests [LOW]

self._mark_consolidation_done(db_conn) now precedes the docstring, turning the docstring into a no-op string literal rather than the function's __doc__. Place the call after the docstring.

**tests** [LOW] `self._mark_consolidation_done(db_conn)` now precedes the docstring, turning the docstring into a no-op string literal rather than the function's `__doc__`. Place the call after the docstring. <!-- wuming:sha256:c4cfa17dc361c6ba3829c23fa7685f169e4127777369ca832466342a90f887bc -->

tests [LOW]

self._mark_consolidation_done(db_conn) was inserted before the docstring, so the docstring is no longer attached to the test function (it is just a discarded expression). Reorder so the docstring remains the first statement.

**tests** [LOW] `self._mark_consolidation_done(db_conn)` was inserted before the docstring, so the docstring is no longer attached to the test function (it is just a discarded expression). Reorder so the docstring remains the first statement. <!-- wuming:sha256:9ad5bb62fdfbf9116ee95198167770295358940634506d20889bd88815eafc09 -->

tests [LOW]

self._mark_consolidation_done(db_conn) precedes the docstring here, demoting the docstring to an unused string literal. Move the helper call below the docstring.

**tests** [LOW] `self._mark_consolidation_done(db_conn)` precedes the docstring here, demoting the docstring to an unused string literal. Move the helper call below the docstring. <!-- wuming:sha256:01a04dd934dab74dfbcaddcfd5b87814c40fc98ad06b2b432050ea2bc0a6ec1b -->

tests [LOW]

self._mark_consolidation_done(db_conn) is placed before the docstring, so the docstring is no longer the function's documentation. Put the call after the docstring.

**tests** [LOW] `self._mark_consolidation_done(db_conn)` is placed before the docstring, so the docstring is no longer the function's documentation. Put the call after the docstring. <!-- wuming:sha256:7c4fd5cf47a45c3b034598c56ee7f31e4fd858f9c71058e9dabfcb60eb2eb8a6 -->
@ -225,3 +227,3 @@
sample_articles: list[Article],
) -> None:
"""Test the title is stripped of padding and CRLF remnants (issue #42)."""
"""Test a padded name renders verbatim without crashing (issue #42 legacy)."""

tests [MEDIUM]

The test's title assertion was dropped when the summary-splitting logic was replaced, but nothing was added in its place: the test now only checks the status code and that the overview text is present, so it no longer verifies the 'renders verbatim' behavior its new docstring claims. Add an assertion on the rendered heading (e.g. the verbatim padded name ' Padded Title ') so this test still exercises its stated purpose.

**tests** [MEDIUM] The test's title assertion was dropped when the summary-splitting logic was replaced, but nothing was added in its place: the test now only checks the status code and that the overview text is present, so it no longer verifies the 'renders verbatim' behavior its new docstring claims. Add an assertion on the rendered heading (e.g. the verbatim padded name ' Padded Title ') so this test still exercises its stated purpose. <!-- wuming:sha256:f6d3da197eeaf29cc9ca78590f62df38ec5ec5794c51395bb72f621f101d0c15 -->
marvin8 marked this conversation as resolved
📝 Address WuMing review: eligibility wording, verbatim title pin, restored docstrings
All checks were successful
/ gitleaks (pull_request) Successful in 30s
/ pr-review (pull_request) Successful in 1m20s
/ checks (pull_request) Successful in 34m26s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m41s
cd51c48831
- docs: 'age floor' misdescribed the bound's direction — the variable is
  a maximum age; the row now also names the subscribed-feed requirement
  and the inclusive boundary.
- tests: the repurposed padded-name test lost its heading assertion; it
  now pins that names render verbatim (no reader-side stripping).
- tests: five tests had the consolidation-pin helper inserted above
  their docstring, demoting the docstring to a no-op literal.
Author
Collaborator

Review wave addressed — all 8 findings fixed in cd51c48

✅ Eligibility wording (4752 · 4753)

"Age floor" is inaccurate … The eligibility conditions are incomplete.

The row now reads "Maximum age (days) for aggregation eligibility" and names all three conditions — subscribed feed, no aggregate membership, created within this many days (inclusive). Both points were right: the old wording inverted the bound and omitted the subscribed-feed requirement.


✅ Verbatim title pin (4754)

The test's title assertion was dropped … nothing was added in its place.

Fair catch — that assertion was a ledgered deviation from the plan and it left the test claiming behaviour it no longer checked. The test now asserts the rendered heading <h2> Padded Title </h2>, pinning that names render exactly as stored (the old reader-side whitespace stripping was blob-format scaffolding, not spec'd behaviour).


✅ Docstrings restored (4755 · 4756 · 4757 · 4758 · 4759)

self._mark_consolidation_done(db_conn) was inserted before the docstring …

Correct, and my bug: the helper insertion for the consolidation pin landed above five docstrings, demoting each to a discarded string literal (which also dodges the project's pydocstyle rules). All five now have the docstring as the first statement. I checked every function in the file with an AST pass — those five were the only affected sites.

### Review wave addressed — all 8 findings fixed in `cd51c48` **✅ Eligibility wording** ([4752](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4752) · [4753](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4753)) > "Age floor" is inaccurate … The eligibility conditions are incomplete. The row now reads "Maximum age (days) for aggregation eligibility" and names all three conditions — subscribed feed, no aggregate membership, created within this many days (inclusive). Both points were right: the old wording inverted the bound and omitted the subscribed-feed requirement. --- **✅ Verbatim title pin** ([4754](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4754)) > The test's title assertion was dropped … nothing was added in its place. Fair catch — that assertion was a ledgered deviation from the plan and it left the test claiming behaviour it no longer checked. The test now asserts the rendered heading `<h2> Padded Title </h2>`, pinning that names render exactly as stored (the old reader-side whitespace stripping was blob-format scaffolding, not spec'd behaviour). --- **✅ Docstrings restored** ([4755](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4755) · [4756](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4756) · [4757](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4757) · [4758](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4758) · [4759](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4759)) > `self._mark_consolidation_done(db_conn)` was inserted before the docstring … Correct, and my bug: the helper insertion for the consolidation pin landed above five docstrings, demoting each to a discarded string literal (which also dodges the project's pydocstyle rules). All five now have the docstring as the first statement. I checked every function in the file with an AST pass — those five were the only affected sites.
forgejo-actions left a comment

WuMing

Found 3 issue(s). See inline comments below.

## WuMing Found **3** issue(s). See inline comments below.
docs/config.md Outdated
@ -13,6 +13,9 @@ Aggregation needs access to an **OpenAI-compatible chat completions API**
| `YUNJIN_LLM_API_KEY` | *none* — **required** for aggregation | API key for the chat completions endpoint. Aggregation fails fast with a clear error when it is unset. |
| `YUNJIN_LLM_BASE_URL` | `https://openrouter.ai/api/v1` | Base URL of the OpenAI-compatible API. |
| `YUNJIN_LLM_MODEL` | `deepseek/deepseek-v4.1-flash` | Model used to group articles and write aggregate titles/overviews. |

docs [LOW]

This entry still says the model writes "aggregate titles", but this PR replaces the summary-derived title with a stable name column that the aggregator writes separately (later passes rewrite only the overview, and the reader titles groups from the column). Update the wording to "write aggregate names and overviews" so the entry matches the new data model.

**docs** [LOW] This entry still says the model writes "aggregate titles", but this PR replaces the summary-derived title with a stable `name` column that the aggregator writes separately (later passes rewrite only the overview, and the reader titles groups from the column). Update the wording to "write aggregate names and overviews" so the entry matches the new data model. <!-- wuming:sha256:fb65f0b0c6ec9835879138ee43ed12e6e35e7b5b17c8cfd64e8bf5c453e66f6c -->
marvin8 marked this conversation as resolved
@ -14,2 +14,4 @@
| `YUNJIN_LLM_BASE_URL` | `https://openrouter.ai/api/v1` | Base URL of the OpenAI-compatible API. |
| `YUNJIN_LLM_MODEL` | `deepseek/deepseek-v4.1-flash` | Model used to group articles and write aggregate titles/overviews. |
| `YUNJIN_ARTICLE_ELIGIBILITY_DAYS` | `30` | Maximum age (days) for aggregation eligibility: an article is sent to the LLM only if it belongs to a subscribed feed, is not already in one of the user's aggregates, and was created within this many days (inclusive). Invalid or below-1 values fall back to the default. |
| `YUNJIN_CONSOLIDATION_INTERVAL_HOURS` | `24` | Minimum hours between a user's consolidation passes, which re-cluster the trailing window in place. Invalid or below-1 values fall back to the default. |

docs [LOW]

"which re-cluster the trailing window in place" is vague: "in place" is undefined, and it omits that eligible articles are re-clustered too (described only on line 18). Consider: "Minimum hours between a user's consolidation passes; a pass re-clusters the trailing YUNJIN_CONSOLIDATION_WINDOW_HOURS window together with any eligible articles."

**docs** [LOW] "which re-cluster the trailing window in place" is vague: "in place" is undefined, and it omits that eligible articles are re-clustered too (described only on line 18). Consider: "Minimum hours between a user's consolidation passes; a pass re-clusters the trailing `YUNJIN_CONSOLIDATION_WINDOW_HOURS` window together with any eligible articles." <!-- wuming:sha256:249347b5b41446dc07d03ad4424fcaaae4eb72a2d5a3df49c76ad0c0243414f4 -->
marvin8 marked this conversation as resolved
@ -60,3 +63,3 @@
|----------|---------|---------|
| `PORT` | `8080` | Port the web entry point listens on (integer 1–65535). |
| `YUNJIN_WORKER_INTERVAL` | `3600` | Seconds between worker loop iterations (fetch → aggregate → extract-tags → extract-media). Positive integer, at most 9 digits. |
| `YUNJIN_WORKER_INTERVAL` | `3600` | Seconds between worker loop iterations (fetch → aggregate (with periodic consolidation) → extract-tags → extract-media). Positive integer, at most 9 digits. |

docs [LOW]

The nested parentheses make this pipeline description harder to read, and burying the new consolidation stage inside them makes it easy to miss. Consider flattening it, e.g. "fetch → aggregate → periodic consolidation → extract-tags → extract-media".

**docs** [LOW] The nested parentheses make this pipeline description harder to read, and burying the new consolidation stage inside them makes it easy to miss. Consider flattening it, e.g. "fetch → aggregate → periodic consolidation → extract-tags → extract-media". <!-- wuming:sha256:82c6c645d34869afa02f495ac7cf0566ce2c742d06ba67cc1a97edb9f9470ef3 -->
marvin8 marked this conversation as resolved
marvin8 approved these changes 2026-10-10 23:05:54 +00:00
Dismissed
📝 Address WuMing docs review: names/overviews, consolidation wording, pipeline line
All checks were successful
/ gitleaks (pull_request) Successful in 16s
/ pr-review (pull_request) Successful in 1m21s
/ checks (pull_request) Successful in 28m47s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m39s
6e06887ba4
- YUNJIN_LLM_MODEL: the model writes aggregate names and overviews (the
  name is a stable column now), not summary-derived titles.
- YUNJIN_CONSOLIDATION_INTERVAL_HOURS: drop the vague 'in place' and say
  what a pass actually re-clusters (window + eligible articles).
- YUNJIN_WORKER_INTERVAL: flatten the nested parentheses so the
  consolidation stage is visible in the pipeline order.
agent-pi dismissed marvin8's review 2026-10-10 23:07:21 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

forgejo-actions left a comment

WuMing

Found 2 issue(s). See inline comments below.

## WuMing Found **2** issue(s). See inline comments below.
@ -14,2 +14,3 @@
| `YUNJIN_LLM_BASE_URL` | `https://openrouter.ai/api/v1` | Base URL of the OpenAI-compatible API. |
| `YUNJIN_LLM_MODEL` | `deepseek/deepseek-v4.1-flash` | Model used to group articles and write aggregate titles/overviews. |
| `YUNJIN_LLM_MODEL` | `deepseek/deepseek-v4.1-flash` | Model used to group articles and write aggregate names and overviews. |
| `YUNJIN_ARTICLE_ELIGIBILITY_DAYS` | `30` | Maximum age (days) for aggregation eligibility: an article is sent to the LLM only if it belongs to a subscribed feed, is not already in one of the user's aggregates, and was created within this many days (inclusive). Invalid or below-1 values fall back to the default. |

docs [MEDIUM]

This row states the condition absolutely — an article is sent to the LLM "only if" it is not already in one of the user's aggregates — but the consolidation rows below (lines 17–18) say a pass re-clusters the trailing window together with any eligible articles. Articles already in the user's aggregates are therefore re-sent to the LLM whenever they fall inside the consolidation window, so the "only if" wording is inaccurate and contradicts the rows immediately following it. Fix by scoping the claim to the incremental path (e.g. "in the incremental pass, an article is sent to the LLM only if ...") or by adding a caveat such as "(during a consolidation pass the trailing window is also re-clustered, including articles already in the user's aggregates)".

**docs** [MEDIUM] This row states the condition absolutely — an article is sent to the LLM "only if" it is not already in one of the user's aggregates — but the consolidation rows below (lines 17–18) say a pass re-clusters the trailing window *together with* any eligible articles. Articles already in the user's aggregates are therefore re-sent to the LLM whenever they fall inside the consolidation window, so the "only if" wording is inaccurate and contradicts the rows immediately following it. Fix by scoping the claim to the incremental path (e.g. "in the incremental pass, an article is sent to the LLM only if ...") or by adding a caveat such as "(during a consolidation pass the trailing window is also re-clustered, including articles already in the user's aggregates)". <!-- wuming:sha256:74c7fe20e703f25d2a243be9832b9eb6ff3c00cca2238218df9fc196df3ab51f -->
@ -463,0 +467,4 @@
db_conn,
user_id=_test_user_id(db_conn),
name="Group Name",
summary="Group Name\n\nA long overview paragraph.",

tests [LOW]

This test exists to verify that reader cards take the group title from the new name column rather than from the summary text, but the setup still embeds the title in the summary ("Group Name\n\nA long overview paragraph."). Since the summary's first line is also 'Group Name', the assertion cannot distinguish the new behavior from a regression back to summary-derived titles, so it passes either way. Update the summary to only the overview (e.g. "A long overview paragraph.") as the sibling tests at lines 194, 233, and 397 already do, so the assertion actually exercises the name column.

**tests** [LOW] This test exists to verify that reader cards take the group title from the new `name` column rather than from the summary text, but the setup still embeds the title in the summary (`"Group Name\n\nA long overview paragraph."`). Since the summary's first line is also 'Group Name', the assertion cannot distinguish the new behavior from a regression back to summary-derived titles, so it passes either way. Update the summary to only the overview (e.g. `"A long overview paragraph."`) as the sibling tests at lines 194, 233, and 397 already do, so the assertion actually exercises the `name` column. <!-- wuming:sha256:f54c132739757951fe2416df37e534273d7b13f2d8f43cfd8a89660e58a62383 -->
📝 Scope eligibility filter to the incremental pass; strengthen title-only test
All checks were successful
/ gitleaks (pull_request) Successful in 16s
/ pr-review (pull_request) Successful in 1m20s
/ checks (pull_request) Successful in 28m59s
/ publish-container (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ container (pull_request) Successful in 1m36s
1ebec4669d
Author
Collaborator

docs/config.md line 16

This row states the condition absolutely — an article is sent to the LLM "only if" it is not already in one of the user's aggregates…

✅ Fixed in commit 1ebec46 — the "only if" claim is now scoped to the incremental pass, with a follow-on sentence noting that a consolidation pass also sends the articles in its trailing window, including ones already in aggregates (verified against _run_consolidation).

[`docs/config.md` line 16](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4801) > This row states the condition absolutely — an article is sent to the LLM "only if" it is not already in one of the user's aggregates… ✅ Fixed in commit `1ebec46` — the "only if" claim is now scoped to the incremental pass, with a follow-on sentence noting that a consolidation pass also sends the articles in its trailing window, including ones already in aggregates (verified against `_run_consolidation`).
Author
Collaborator

tests/test_web.py line 470

This test exists to verify that reader cards take the group title from the new name column rather than from the summary text…

✅ Fixed in commit 1ebec46 — the summary is now the overview only ("A long overview paragraph."), matching the sibling tests. RED-verified: with a simulated regression to summary-derived titles the strengthened test fails where the old one passed vacuously.

[`tests/test_web.py` line 470](https://forge.marvin8.zone/marvin8/yunjin/pulls/79#issuecomment-4802) > This test exists to verify that reader cards take the group title from the new `name` column rather than from the summary text… ✅ Fixed in commit `1ebec46` — the summary is now the overview only (`"A long overview paragraph."`), matching the sibling tests. RED-verified: with a simulated regression to summary-derived titles the strengthened test fails where the old one passed vacuously.
marvin8 approved these changes 2026-10-11 00:17:52 +00:00
marvin8 manually merged commit 709f837d10 into main 2026-10-11 00:18:34 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
marvin8/yunjin!79
No description provided.