Skip to content

feat(prompt): extraction focus and content requirements settings - #895

Open
green-dalii wants to merge 4 commits into
mainfrom
feat/755b-prompt-focus
Open

green-dalii wants to merge 4 commits into
mainfrom
feat/755b-prompt-focus

Conversation

@green-dalii

Copy link
Copy Markdown
Member

Split out of #755. The code is @weqoocu's; the split, and the one removal described below, are mine. The commit carries their author line.

Two free-text fields that shape what the model looks for and writes about, without touching any contract the plugin owns:

  • extractionFocus — appended to the extraction prompt's Extraction Scope block as a domain-priority hint. It narrows what the model looks for; it does not replace the JSON protocol, the classification rules or the tag vocabulary.
  • contentRequirements — appended to entity/concept page generation through a {{content_requirements}} placeholder after the merge-strategy block. It controls emphasis and structure; citation, wikilink, merge and anti-hallucination rules stay binding.

Empty is the default, and empty adds nothing, so every vault that never configures these gets byte-identical prompts and keeps its prefix cache. Both fields are capped at 4,000 characters, so a runaway paste cannot bloat every batch.

What I removed, and the evidence

The submitted change also carried a per-topic override layer: settings.wikiTopics, activeTopicId, a WikiTopic interface, and a resolution path that preferred a topic's own fields over the global ones.

It could not run. Nothing in the settings UI creates or edits a wikiTopics entry, and nothing elsewhere writes the field, so it was always []. activeTopicId had no reader and no writer. The only trace in the UI was a description string naming a scope the user could not reach.

The key difference from "unused API surface": the submitted code was activeTopic(settings, ...).find(...) — a branch that every prompt build evaluated and that could never be true. Shipping it means every future reader has to prove unreachability again before trusting the feature. The repository's rule for this is wire it or delete it before a release.

This is a deletion of your work, @weqoocu, and it is reversible by adding the picker that writes the field. I have recorded it as an issue rather than dropping it silently; if you want the layer here instead, say so and the third PR goes up as-is with the picker alongside it.

i18n

Eight new keys in all eleven locales, key sets identical so the type barrel stays honest. Japanese, Korean, German, French, Spanish, Portuguese, Italian and Russian currently carry the English text; Chinese and Traditional Chinese are translated. That gap is real — it is called out here rather than left for a user to discover, and translating it is a separate, reviewable change.

Gate 4: Performance

Dim Status Notes
CPU ✅ Two trim + length checks per prompt build. The clamp allocates only when the field exceeds 4,000 chars
Memory ✅ Two strings on the settings object; no cache, no map
IO ✅ No change
Network ✅ No change
Token ⚠️ This is the point of the feature, so it is worth naming: a configured focus or requirements block adds its own text to every extraction and every page generation. The 4,000-char cap bounds it, and the default (empty) adds zero. A user who fills both fields close to the cap adds roughly 1,500-3,000 tokens per call — that is their choice, and it is visible in the settings panel

Credit, stated rather than assumed

If this merges by squash, the merge commit's author becomes the PR author — me — and @weqoocu's contribution would read as mine. A rebase merge keeps their author line. That decision belongs to the maintainer at merge time, not to me.

Gate 1: lint 0, tsc 0, build clean, 369 test files / 5,047 tests, css-lint 0.

Split out of #755, which bundled this with a machine-name guard that now lives
in its own PR. The code is @weqoocu's; the split, and the removal described
below, are mine.

Two free-text fields that shape what the model looks for and writes about,
without touching any contract the plugin owns:

- `extractionFocus` is appended to the extraction prompt's Extraction Scope
  block as a domain-priority hint. It narrows what the model looks for; it does
  not replace the JSON protocol, the classification rules or the tag vocabulary.
- `contentRequirements` is appended to entity/concept page generation through a
  `{{content_requirements}}` placeholder after the merge-strategy block. It
  controls emphasis and structure; citation, wikilink, merge and
  anti-hallucination rules stay binding.

Empty is the default and empty adds nothing, so every vault that never configures
these gets byte-identical prompts and keeps its prefix cache.

## What was removed from the original commit, and why

The submitted change also carried a per-topic override layer — `settings.wikiTopics`,
`activeTopicId`, a `WikiTopic` interface, and a `promptFocus.ts` resolution path
that preferred a topic's own fields over the global ones. It could not run:

- nothing in the settings UI creates or edits a `wikiTopics` entry, and nothing
  elsewhere writes the field, so it was always `[]`;
- `activeTopicId` had no reader and no writer;
- the only trace of the topic scope in the UI was a description string naming a
  scope the user could not reach.

That is unreachable code by construction, and the repository's dead-code rule is
to wire it or delete it before a release ships it. It is deleted here and recorded
as an issue, so bringing it back means bringing the picker with it.

## i18n

Eight new keys in all eleven locales, with the key sets identical so the type
barrel stays honest. Japanese, Korean, German, French, Spanish, Portuguese,
Italian and Russian currently carry the English text; Chinese and Traditional
Chinese are translated. That gap is real and is called out rather than hidden.

## Credit

The commit author line is @weqoocu's. If this merges by squash the merge commit's
author becomes the PR author — me — which would understate their work; that choice
belongs to the maintainer at merge time.

Gate 1: lint 0, tsc 0, build clean, tests pass, css-lint 0.
…ng locales

The feature shipped with the English text in eight of the eleven locales. Chinese
and Traditional Chinese were translated; Japanese, Korean, German, French,
Spanish, Portuguese, Italian and Russian carried the English source verbatim, so
a user in any of those languages read an English settings panel describing a
feature whose every neighbour was in their own language.

56 strings: 7 keys x 8 locales.

The wording follows each locale's existing terms rather than a fresh glossary —
`抽出` / `추출` / `Extraktion` / `Extraction` / `Extracción` / `Extração` /
`Estrazione` / `Извлечение` for the extraction section, and each file's own
wording for "disabled", "citation" and "anti-hallucination" as they appear in
the neighbouring settings. The placeholder examples keep the same shape as the
English source (main characters, game modes, weapons, battle pass), so a reader
sees a concrete domain rather than a transliteration.

Two things this commit does not do:

- It does not touch the `lintTagViolation*` cluster, which is untranslated in
  the same eight locales and predates this feature. That is a separate change
  with its own PR, because a fix that bundles an unrelated gap cannot be
  reviewed on its own terms.
- It does not add a guard that would have caught the original gap. The parity
  test checks that every key EXISTS in every locale; it cannot see that a key
  holds the English text. A guard is proposed separately, with the measurement
  that shows what it can and cannot flag.

Gate 1: lint 0, tsc 0, build clean, tests pass, css-lint 0.
@green-dalii

Copy link
Copy Markdown
Member Author

Follow-up: the i18n gap I flagged in the PR body is now closed.

56 strings — 7 keys × 8 locales — for Japanese, Korean, German, French, Spanish, Portuguese, Italian and Russian. Chinese and Traditional Chinese were already translated.

The wording follows each locale's existing terms rather than a fresh glossary. The extraction section is 抽出 in ja, 추출 in ko, Extraktion in de, Extraction in fr, Extracción in es, Extração in pt, Estrazione in it, Извлечение in ru — the terms each file already uses for its own extraction settings. The placeholder examples keep the English source's shape (main characters, game modes, weapons, battle pass) so the reader sees a concrete domain instead of a transliteration.

Two things this does not do

The lintTagViolation* cluster is still untranslated in the same eight locales. It predates this feature — Retagging {current}/{total}, Retag failed for {path}, Retag complete…, 🏷️ Retag {count} page(s) with LLM and two others, 6 keys × 8 locales = 48 strings. I am not bundling them here: a fix that carries an unrelated gap cannot be reviewed on its own terms. It needs its own PR.

No guard test is added in this PR. The parity test checks that every key exists in every locale. It cannot see that a key holds the English text — which is exactly how the 56 strings above shipped with the feature and passed every check. A guard is worth adding, but the threshold has to be chosen against measurement rather than guessed: a blanket "must differ from English" rule would flag the ~5 % of strings that are legitimately identical across locales (language names, API Key, AWS SSO, format templates). The measured candidate is "English description strings of ≥40 characters", which currently flags exactly the two lintTagViolation keys and nothing else — clean once those are translated.

Gate 1: lint 0, tsc 0, build clean, 369 test files / 5,047 tests, css-lint 0.

@KvikullSigurdur

Copy link
Copy Markdown

I ran this branch at 56ebcd8 with one extra test, and found two places where contentRequirements does not reach the written page, plus one design question about page types. Sharing in case it is useful before merge.

The test uses the setting's own placeholder as the requirement (contentRequirementsPlaceholder, en.ts:414, without the "e.g."): Cite the source episode/chapter when known; use "reportedly/inferred" for unsourced lore; keep a Risk Notes section where applicable.

It drives createOrUpdateEntityPage with a fake model client that records every prompt and returns a page with a ## Risk Notes section.

Check Result
The creation prompt carries the requirement ✓
The written page keeps the ## Risk Notes section the model wrote ✗
A merge into an existing page carries the requirement ✗

1. A requested section is removed after generation

create-page.ts:245 runs stripUnknownSections against the built-in section labels. Any heading outside that list is dropped, so ## Risk Notes is gone from the written page while Description, Related Entities, Related Concepts and Mentions in Source stay. merge-page.ts:353 and related-page.ts:174 apply the same filter.

So the placeholder's own example cannot take effect. The setting's description also lists "structure" among the things it controls (contentRequirementsDesc, en.ts:413, and prompt-focus.ts:71).

2. Merges run without the requirement

{{content_requirements}} is in generateEntityPage and generateConceptPage only (generation.ts:19, :79), and create-page.ts:210 is the only call that fills it. A merge into an existing page sends two prompts, the merge triage and mergeEntityPage, and neither carries the requirement. So the setting shapes a page when it is created, and every later source that touches the page is written without it.

import { describe, it, expect } from 'vitest';
import { createOrUpdateEntityPage, type CreatePageContext } from '../../../wiki/page-factory/create-page';
import { createMockEntity } from '../../__support__/factories';
import type { LLMWikiSettings, WikiPageRef } from '../../../types';
import { mockExistingWikiPages } from '../../__support__/engine-context';

// The settings placeholder from src/texts/en.ts, without its "e.g." prefix.
const REQUIREMENT = 'Cite the source episode/chapter when known; use "reportedly/inferred" for unsourced lore; keep a Risk Notes section where applicable';
const MODEL_REPLY = '---\ntype: entity\ntags: [person]\naliases: ["Ada"]\n---\n\n# Ada Lovelace\n\n## Description\nMathematician.\n\n## Risk Notes\n- Some attributions are reportedly disputed.\n\n## Related Entities\n- [[Charles Babbage]]';
const EXISTING_PAGE = '---\ntype: entity\ncreated: 2026-07-10\nupdated: 2026-07-10\nsources:\n - "[[older-note]]"\ntags: [person]\n---\n\n# Ada Lovelace\n\n## Description\nOlder body.\n';

function makeCtx(files: Record<string, string> = {}) {
const store = new Map(Object.entries(files));
const prompts: string[] = [];
const ctx = {
app: { vault: { getMarkdownFiles: () => [], read: async (f: { path: string }) => store.get(f.path) ?? '' } },
settings: { wikiFolder: 'wiki', wikiLanguage: 'en', slugCase: 'preserve', disableThinking: false, contentRequirements: REQUIREMENT } as LLMWikiSettings,
async tryReadFile(p: string) { return store.get(p) ?? null; },
async createOrUpdateFile(p: string, c: string) { store.set(p, c); },
getClient: () => ({ createMessage: async (args: { messages: Array<{ content: unknown }> }) => {
prompts.push(args.messages.map(m => String(m.content)).join('\n'));
return MODEL_REPLY;
} }),
buildSystemPrompt: async () => 'system',
getExistingWikiPages(): Promise<WikiPageRef[]> { return mockExistingWikiPages(this)(); },
} as unknown as CreatePageContext;
return { ctx, store, prompts };
}

const SOURCE = { path: 'notes/ada.md', basename: 'ada.md' };

describe('#895 contentRequirements', () => {
it('the creation prompt carries the requirement', async () => {
const { ctx, prompts } = makeCtx();
await createOrUpdateEntityPage(ctx, createMockEntity({ name: 'Ada Lovelace' }), undefined, SOURCE);
expect(prompts.some(p => p.includes(REQUIREMENT))).toBe(true); // passes
});

it('the written page keeps the Risk Notes section the model wrote', async () => {
const { ctx, store } = makeCtx();
const r = await createOrUpdateEntityPage(ctx, createMockEntity({ name: 'Ada Lovelace' }), undefined, SOURCE);
expect(store.get(r.path!)).toContain('## Description'); // passes
expect(store.get(r.path!)).toContain('## Risk Notes'); // fails
});

it('a merge carries the requirement', async () => {
const { ctx, prompts } = makeCtx({ 'wiki/entities/Ada-Lovelace.md': EXISTING_PAGE });
const r = await createOrUpdateEntityPage(ctx, createMockEntity({ name: 'Ada Lovelace' }), undefined, SOURCE);
expect(r.created).toBe(false); // passes: this is a merge
expect(prompts.map(p => p.includes(REQUIREMENT))).toEqual([true, true]); // fails: [false, false]
});
});

green-dalii added a commit that referenced this pull request Oct 27, 2026
A source note whose own filename is a machine identifier produced junk pages. When the ingested note is named `01988c6e_86e9_7b79_83b5_c8c4b354fdc9.md` or `202511042307.md`, the model sometimes extracted the source DOCUMENT itself as an entity and lifted the filename as its name — the result is `entities/<uuid>.md`, a page whose body merely re-narrates the summary it came from.

`isSourceSelfReference` rejects those names at normalization time, before a page exists:

- machine shapes are rejected regardless of what the source file is called — a UUID with `-`/`_` separators, a 12- or 14-digit timestamp, and any run of 13 or more digits optionally followed by a `_`/`.` plus a hex tail, which covers IM voice-note message IDs like `1921147873752416344` and snowflake-style keys;
- a verbatim copy of the source basename is rejected only when it carries a plugin-style `_` plus 6-or-more-hex-digit tail, so `TNF-α` extracted from `TNF-α.md` survives and a drama name from `庆余年_8a53fa.md` survives.

Phone-sized runs (11 digits or fewer) and years stay allowed, because those are legitimate names.

**The wiring is part of this PR, and that is the point.** The original commit added the predicate and its regexes but left the two `.filter(...)` calls that use them to the commit that followed. Taken alone it would have shipped an exported function that nothing calls, with tests that pass by calling the function directly — a guard that is not on the path. The diff shows the two filters.

The prompt-level layer ("do not extract the source document, do not use its filename as a name") is in the sibling PR #895, because it is a prompt change and this is a structural filter.

**Credit.** The two commits in this branch are @weqoocu's work and carry their author line. This squash names them; the merge commit's author is me, which is a GitHub property of squashing, not a judgement about who wrote the code.

Gate 1: lint 0, tsc 0, build clean, 367 test files / 5,038 tests, css-lint 0.
green-dalii added a commit that referenced this pull request Oct 27, 2026
…nslated string

Six `lintTagViolation*` strings were English in eight of the eleven locales — 48 strings. A user in zh, zh-Hant, ja, ko, de, fr, es or pt pressed a button that said "Retag", and read a progress line in English while every neighbouring setting was in their own language.

| key | was |
|---|---|
| `lintTagViolationFiring` | `Retagging {current}/{total}: {path}` |
| `lintTagViolationFailed` | `Retag failed for {path}: {error}` |
| `lintTagViolationFixed` | `Retag complete. Fixed {fixed}/{total} page(s).` |
| `lintTagViolationFixedNone` | `Retag complete. No pages needed fixing (LLM kept current tags).` |
| `lintTagViolationItem` | `- [[{path}]] — invalid: {tags}` |
| `lintTagViolationRetagBtn` | `🏷️ Retag {count} page(s) with LLM` |

Italian and Russian had these translated. That is how the gap survived: two locales out of eleven were correct, and the check looked at the whole set.

## The guard that would have caught it

The existing parity test asks whether every key **exists** in every locale, because `getText()` falls back to English for a *missing* key. A locale that **copies** the English string is not a fallback — it is a file that reads as English on purpose, and every check passes.

The new test asks the other question: for every English prose string of at least 30 characters, does each non-English locale hold something different?

**The threshold is measured, not guessed.** At 30 characters exactly five strings across all eleven locales are legitimately identical to English — three format templates (`[{step}/{totalSteps}] {type}: {name}` and two lint-line templates, where the prose is a placeholder pattern and the wording lives in the substituted values) and two example tag-name lists (`person, organization, project, place`, which are sample tag NAMES rather than prose). Each is listed in the test with its reason, because an exemption without a reason is a hole with a comment.

At 40 characters the count drops to zero — but 40 misses `Retag failed for {path}: {error}` at 32 characters, which is one of the strings that actually shipped untranslated. So 30, with the exemptions the measurement showed are needed.

**What it catches, and what it cannot.** Of the prompt-customization gap in #895 it catches 5 of 7 keys. It misses the two headings under 20 characters, where an identical string is a weak signal rather than a proof. That limit is written into the test.

**The exemptions cannot rot.** A second test fails when an exemption is no longer needed anywhere, so the list cannot quietly become a place to hide new gaps. Its rule is "at least ONE locale still holds it verbatim" — `zh` writes `[{step}/{totalSteps}] {type}:{name}` with a full-width colon, which is punctuation localization, not translation. The first version of that rule was stricter and failed on exactly that string.

**RED, then GREEN.** With `ja` reverted to its English strings the guard failed and named all six keys; with them restored, 144 of 144 pass.

**What this does not claim.** It does not assert that every locale is fully translated — that claim is not true and this test does not make it. What it enforces is a measured floor: no English prose of 30 characters or more, with five documented exceptions.

Gate 1: lint 0, tsc 0, build clean, 367 test files / 5,044 tests, css-lint 0.
@green-dalii

Copy link
Copy Markdown
Member Author

@KvikullSigurdur — both findings reproduce here. I ran them independently rather than taking the report as read, and they are correct.

Reproduction (my own file, src/__tests__/wiki/page-factory/content-requirements-effect.test.ts, same shape as yours):

× FINDING 1: the written page keeps the Risk Notes section the model wrote
  AssertionError: expected '---\ntype: entity\ncreated: 2026-10-2…' to contain '## Risk Notes'
× FINDING 2: a merge carries the requirement
  AssertionError: expected [ false, false ] to deeply equal [ true, true ]

Mechanism, confirmed at the source:

  • stripUnknownSections(content, Object.values(labels)) — create-page.ts:240, merge-page.ts:353, related-page.ts:174. It drops any ## heading whose canonical label is not in SECTION_LABELS[wikiLanguage]. The section list is plugin-owned and language-scoped.
  • {{content_requirements}} appears in generation.ts:19 and :79 only. create-page.ts:210 is the only place that fills it. The merge path renders mergeAnalysis / mergeEntityPage / mergeConceptPage / appendToReviewedPage / updateRelatedPage and passes no such key, so renderTemplate never sees it.

Neither defect came from the split. The original branch had the same shape: generation.ts carried the placeholder twice and merge.ts carried it zero times — merge.ts was never touched by the commit that introduced the feature. My split moved code; it did not create this.

The review gap was mine. The 14 tests in #895 assert that the template CONTAINS {{content_requirements}} and that the rendered output does not contain the literal placeholder. Both pass while the feature does nothing to a written page. That is the placeholder-contract test standing in for an effect test — the same shape this project keeps hitting, and I did not catch it on review. The effect test above is the one that should have existed.

The two findings are different in kind

Finding 2 is a wiring gap and should be fixed. The setting shapes a page at creation and is silently absent on every later write. A page built from five sources has the requirement in the first and not in the other four — a page that is inconsistent with itself, which is worse than the setting not existing.

Finding 1 is a promise that the mechanism cannot keep, and that is a design decision, not a bug to patch.

The section list is plugin-owned. mergeEntityPage even enforces it explicitly (- ## {{section_description}}: …), and mergeAnalysis reads the same labels. So a page cannot gain a section from a prompt, and the project's own schema rule says the schema owns page structure while the plugin injects runtime parameters. Letting a prompt invent a section would break that separation, and it would also break the merge path, which must know the label set to strip and re-assert headings.

So there are two coherent answers, and this needs a decision rather than a patch:

  • A. Narrow the promise. contentRequirements controls emphasis and content within the known sections; it does not add sections. Then the description and the placeholder example change, and the setting still delivers something real — the merge wiring is fixed, and the model is told to weight, emphasise and cover aspects inside ## Description, ## Related Entities and the rest.
  • B. Let the user declare sections. The setting gains a way to name extra sections, and they join the allowed label set for the write. That is a schema-layer change: the section list stops being language-scoped and fixed, which touches stripUnknownSections, canonicalizeSectionHeaders, the merge triage prompt and the related-sections logic. It is a bigger change than this PR, and it deserves its own issue and its own measurement.

I lean towards A for this PR, with B recorded as a separate piece of work — but @weqoocu wrote the feature and the call is theirs.

One question the report opens but does not write out: you mentioned "one design question about page types". It is not in the body, so I do not have it. If it is the A-versus-B question above, I have stated it. If it is something else — how a custom section interacts with the per-type label set, say — please write it out, because that is the part I would otherwise guess at.

What is not blocking the release

contentRequirements does not exist on main — zero hits in types.ts, no prompt-focus.ts. It ships in this PR, which is scheduled for v1.30.0 MINOR. Nothing here affects 1.29.0.

Where this leaves the PR

Two of the three assertions in the original body are still mine to fix, and I will not merge this with the placeholder test as the only evidence:

  1. The merge path carries the requirement — I will add {{content_requirements}} to the merge prompts and to appendToReviewedPage / updateRelatedPage, and fill it at the merge call sites.
  2. The effect test exists, so a placeholder-only test can no longer be mistaken for a working feature.

The section-list question is @weqoocu's to answer, and the description text depends on it. I am holding the merge until that is settled, because the honest options produce different descriptions and I would rather write one final text than two.

The reproduction test is on my branch, not this one, so this PR's CI stays green while the decision is open. Say the word and I push it here as the RED that drives the fix.

…older

The 14 tests that came with this feature assert that the prompt template CONTAINS
{{content_requirements}} and that the rendered output does not contain the
literal placeholder. Both pass while the setting does nothing to a written page.

This is the effect test that should have existed: it drives the page factory
with a fake model client and asks what the file on disk contains.

RED on the current code, two failures:

1. A section the model wrote is gone from the file. stripUnknownSections drops
   every heading outside SECTION_LABELS, so a section named in the setting's own
   placeholder example cannot exist on the page.
2. A merge sends two prompts and neither carries the requirement:
   [false, false] where [true, true] was expected.

Finding 2 is a wiring gap and this test drives its fix. Finding 1 is a
promise the mechanism cannot keep — the section list is plugin-owned — so it
waits on a decision about what the setting may promise.

Reported and independently reproduced by @KvikullSigurdur on the PR.
@KvikullSigurdur

Copy link
Copy Markdown

Sorry about the misleading page types question, it is something I had in mind but then edited out forgetting I had foreshadowed it. I started going over issues because I like the plugin a lot but would like a more granular control of the structure (sections) created for each note.

I am not quite caught up enough on the architecture to weigh in on #873, but the idea came up that if this PR meant to introduce the feature of letting users define sections, it might be nice to control sections per page type. Events pages could have a timeline section, projects could have risks etc.

The 14 tests that shipped with the feature assert that the prompt template
CONTAINS {{content_requirements}} and that the rendered output does not
contain the literal placeholder. Both pass while the setting does nothing to
a written page, so neither question is about effect.

This is the effect test: it drives the page factory with a recording fake
client and asks what the file on disk contains.

  the creation prompt carries the requirement            passes
  the written page keeps the ## Risk Notes section       fails
  a merge carries the requirement  [false,false]         fails

Both failures are real, both were reported by @KvikullSigurdur, and both also
exist on the original #755 branch, so the split did not introduce them.

They are pinned with `it.fails` rather than committed red. A red assertion
records the defect once and then blocks every unrelated change; `it.fails`
records it, keeps the signal in the suite, and turns into a failure the
moment the behaviour is fixed, which is how the fix announces that the marker
has to come off. Verified that the trap fires: an `it.fails` around a passing
assertion reports "1 failed".

Finding 2, the merge path, is a wiring gap and its fix is agreed. Finding 1 is
a promise the mechanism cannot keep -- the section list is plugin-owned -- so
it waits on the decision in #902, which also carries @KvikullSigurdur's
per-page-type extension.
@green-dalii

green-dalii commented Oct 27, 2026 •

Copy link
Copy Markdown
Member Author

@KvikullSigurdur
Thanks for the correction, and for going through the issues — that is useful in its own right, not a distraction.

Your two findings reproduce here independently, and they are now pinned in the branch. content-requirements-effect.test.ts drives the page factory with a recording fake client and reads the file on disk, instead of checking that a prompt template contains a placeholder. Both are committed as expected failures rather than left red, so the suite stays green and each one flips the moment it is fixed — which is how we will know the marker has to come off.

I recorded the whole thing, your per-page-type idea included, in #902. The short version of why it is an issue and not a patch in this PR:

So your request is written down where it will actually be acted on.

Please keep sending reports like this one. The reproduction and the file:line references made it cheap to confirm, and any other friction you run into is worth a note — issues and PR threads are both fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants