Skip to content

fix(postgres): embed through get_embedding_function(), not chromadb's default (#413) - #463

Merged
jphein merged 2 commits into
mainfrom
fix/413-postgres-embedding-resolver
Sep 11, 2026
Merged

jphein merged 2 commits into
mainfrom
fix/413-postgres-embedding-resolver

Conversation

@jphein

@jphein jphein commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

What

backends/postgres.py::_embed now delegates to mempalace.embedding.get_embedding_function() instead of constructing its own chromadb.utils.embedding_functions.DefaultEmbeddingFunction().

Why — three defects compounding in one eight-line function

Every embedding-layer improvement in the project lands inside that resolver, so this backend silently missed all of them.

1. MEMPALACE_EMBEDDING_MODEL was ignored on this backend's write path (_prepare_write_inputs) and query path. The config layer resolves correctly — describe_device() reports the endpoint and get_embedding_function() returns OpenAICompatEmbeddingFunction — but _embed never asked, so every vector was computed by a local CPU embedder. Measured on two long-running installs: the configured remote embedding server served 9 texts in 18.3 hours of near-continuous mining. Same class of failure as MemPalace#2324; the external-embedding-API support added in MemPalace#1559 never reached this backend.

2. The ORT intra_op_num_threads cap from MemPalace#1068 was bypassed. That fix lives in _build_ef_class/_MempalaceONNX, wired up only inside the resolver. Measured on a 48-core host: instantiating one such embedder adds ~49 OS threads; daemon CPU averaged 210-265% over 18 hours and was sampled at 2550% mid-mine.

3. DefaultEmbeddingFunction rebuilt the ONNX session on every call. In chromadb 1.5.9 it is not ONNXMiniLM_L6_V2 — it is a separate class in chromadb/api/types.py whose entire __call__ body is return ONNXMiniLM_L6_V2()(input), and ONNXMiniLM_L6_V2.model is a per-instance cached_property. So the module-global _embedder cached an object that held no model.

via DefaultEmbeddingFunction :  0.706s / 0.711s / 0.713s   <- flat, no warm-up ever
via a reused instance        :  1.257s / 0.532s / 0.539s

At DRAWER_UPSERT_BATCH_SIZE = 1000 that is one full session rebuild per 1000 chunks, each spawning and discarding a ~47-thread pool. Per-batch embed time after delegating: 0.706s → 0.016s.

The stated rationale still holds

The old docstring said: reuse Chroma's default local embedding function so the PostgreSQL backend matches the zero-API embedding model already used by the default backend without adding a second ML dependency stack. Delegating satisfies that completely — with no embedding configuration set, the resolver still returns a local ONNX MiniLM embedder. No new dependency, no change to default behaviour. It diverges only where an operator has explicitly configured something else, which is the entire point of MEMPALACE_EMBEDDING_MODEL.

Two deliberate design calls

Not declaring requires_explicit_embeddings. It is architecturally tidier and the other backends do it, but it only takes effect at palace.get_collection() — and mcp_server._get_collection_postgres() calls PostgresBackend.get_collection directly, while convo_miner.mine_sessions() imports mcp_server._get_collection. Both bypass the wrap point, so the capability alone (with _embed raising) would break the MCP server and the miner. Delegating inside _embed fixes every route, wrapped or not, and cannot break a caller that already worked.

Reusing embedding_wrapper._embed_texts rather than re-deriving the conversion. The postgres vector literal formats with %f, and np.float32 scalars do not survive it — that subtlety is documented at length in _embed_texts and should have exactly one home rather than a second copy that can drift.

The new failure mode this change makes reachable, and the guard for it

Before this change _embed could only ever produce EMBEDDING_DIM (384) vectors. Routing through the resolver means an operator can now configure a 768-dim model against a vector(384) column. pgvector's own error names neither the model nor the configuration that selected it, so _warn_on_dimension_mismatch names the model, the produced width, and the column width — once, on the batch that discovers it. Postgres stays the authority on whether the write is legal; this adds no second place that can refuse one.

(Vector-space safety, per the issue: on 500 real stored documents, chromadb's DefaultEmbeddingFunction vs an openai-compat endpoint serving the same all-MiniLM-L6-v2 gave min cosine 0.999999488, k-NN top-20 id-set overlap mean 0.997 — identical space, no re-embedding required there. That will not hold for every configured endpoint, which is what MemPalace#1561/MemPalace#1724 identity enforcement is for.)

Blast radius

mempalace/backends/postgres.py only. The module-global _embedder is deleted rather than moved — get_embedding_function() already caches under a lock.

Tests

tests/test_postgres_embedding_resolver.py — 6 cases:

  • the embedder is constructed once across three _embed calls (defect 3 — this is the 0.706s-flat measurement, expressed structurally)
  • DefaultEmbeddingFunction is never constructed directly
  • the configured embedding function is the one that actually runs (defect 1)
  • plain Python floats come back, not numpy scalars
  • an empty batch never triggers a model load
  • the dimension mismatch is named exactly once, not per batch

All watched failing against main first, and re-verified red after the harness was finalized (4 of 6 red; the float-conversion and empty-batch cases are existing-behaviour guards).

One harness note worth flagging for future test authors: conftest's autouse _stable_embedding_function_for_tests replaces both embedding_wrapper._embed_texts and embedding.get_embedding_function for every module outside its opt-out list. Those stubs sit exactly where this module's subject does and would have silently hidden every assertion here (they did, on the first run). The fixture captures the real functions at import time — collection runs before autouse fixtures — and restores them for this module only. No native ONNX is loaded either way: every test supplies its own embedder.

Full suite: 6821 passed, 82 skipped, 115 deselected. (5 pre-existing test_init_filters_sys_path_from_leaked_pythonpath failures reproduce on an unmodified branch in any git worktree — #454, unrelated.)

ruff check and ruff format --check clean; scripts/check-docs.sh green after all three renderers. Rebased onto main after #453 and #464 landed.

Arms #468

Delegating is what makes an embedder swap possible on this backend, and #468 covers the gap that leaves: postgres.py is the only vector backend with no stored embedder identity, so a swap to a same-width model after this lands is silent — the dimension guard added here catches a width change, and nothing catches a model change at the same width.

Production behaviour is unchanged by this PR: MEMPALACE_EMBEDDING_MODEL is unset in the environment, so the resolver returns the same local ONNX MiniLM embedder the old code constructed directly. The identity work belongs in #468 with MemPalace#1561/MemPalace#1724, not here.

Part of #413
Arms #468

Copilot AI lite review requested due to automatic review settings September 11, 2026 00:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: a94ab069-9d7f-4d14-b5c1-e5c5bc2dcf12

📥 Commits

Reviewing files that changed from the base of the PR and between d8564fc and d6ad8e9.

📒 Files selected for processing (7)
  • CLAUDE.md
  • FORK_CHANGELOG.md
  • README.md
  • docs/fork-changes.yaml
  • mempalace/backends/postgres.py
  • tests/test_postgres_embedding_resolver.py
  • website/public/llms-full.txt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

jphein and others added 2 commits September 10, 2026 19:59
… default

backends/postgres.py::_embed constructed its own DefaultEmbeddingFunction and
never called mempalace.embedding.get_embedding_function(). Every
embedding-layer improvement in the project lands inside that resolver, so this
backend silently missed all of them. Three defects compounded in one
eight-line function:

1. MEMPALACE_EMBEDDING_MODEL was ignored on this backend's write path
   (_prepare_write_inputs) and query path. The config layer resolves fine --
   describe_device() reports the endpoint and get_embedding_function() returns
   OpenAICompatEmbeddingFunction -- but _embed never asked, so every vector was
   computed by a local CPU embedder. Measured on two long-running installs: the
   configured remote embedding server served 9 texts in 18.3 hours of
   near-continuous mining.

2. The ORT intra_op_num_threads cap from MemPalace#1068 is wired up only inside
   get_embedding_function() (via _build_ef_class/_MempalaceONNX), so calling
   DefaultEmbeddingFunction() directly skipped it. Measured on a 48-core host:
   one such embedder adds ~49 OS threads; daemon CPU averaged 210-265% over 18
   hours and was sampled at 2550% mid-mine.

3. In chromadb 1.5.9 DefaultEmbeddingFunction is not ONNXMiniLM_L6_V2 -- its
   whole __call__ body is `return ONNXMiniLM_L6_V2()(input)`, and .model is a
   per-*instance* cached_property. The module-global _embedder therefore cached
   an object holding no model, and every _embed() call rebuilt the entire ONNX
   session: 0.706s / 0.711s / 0.713s across three identical calls -- perfectly
   flat, no warm-up ever -- against 1.257s / 0.532s / 0.539s for a genuinely
   reused instance. At DRAWER_UPSERT_BATCH_SIZE = 1000 that is one session
   rebuild per 1000 chunks, each spawning and discarding a ~47-thread pool.

The docstring's stated rationale -- match the default backend's zero-API local
model without a second ML dependency stack -- is fully satisfied by delegating:
with no embedding configuration set the resolver still returns a local ONNX
MiniLM embedder, adds no dependency, and does not change default behaviour. It
diverges only where an operator explicitly configured something else, which is
the point of MEMPALACE_EMBEDDING_MODEL.

The numpy-to-float conversion is reused from backends/embedding_wrapper rather
than re-derived: the postgres vector literal formats with %f and np.float32
scalars would not survive it, and that subtlety should have one home. The
module-global _embedder is deleted rather than moved -- get_embedding_function
already caches under a lock.

Not declaring requires_explicit_embeddings: it only takes effect at
palace.get_collection(), and mcp_server._get_collection_postgres() and
convo_miner.mine_sessions() both bypass that wrap point, so the capability
alone would break the MCP server and the miner. Delegating inside _embed fixes
every route, wrapped or not.

Routing through the resolver makes one new failure mode reachable: _embed
could previously only ever produce EMBEDDING_DIM vectors, and can now produce
any width against a vector(384) column. _warn_on_dimension_mismatch names the
model, the produced width and the column width once, because pgvector's own
error names none of them. Postgres stays the authority on whether the write is
legal; this only makes the cause legible.

Blast radius: mempalace/backends/postgres.py only.

Tests: tests/test_postgres_embedding_resolver.py, 6 cases -- the embedder is
constructed once across three embeds (defect 3), DefaultEmbeddingFunction is
never constructed directly, the configured embedding function is the one that
runs (defect 1), plain Python floats come back, an empty batch never triggers
a model load, and the dimension mismatch is named exactly once.

Part of #413
Fixes #413

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Entry + the three renderers, plus the README/CLAUDE test count moved
7057 -> 7063 for the 6 new cases. Rebased onto d8564fc; the entry's
commit hash tracks the rebased code commit so check-docs's
hash-resolution check stays green.

The entry also records the same-width embedder-identity gap this change
leaves open, tracked in #468.

Part of #413

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jphein
jphein force-pushed the fix/413-postgres-embedding-resolver branch from d7b5124 to d6ad8e9 Compare September 11, 2026 03:05
@jphein
jphein merged commit 4975b0a into main Sep 11, 2026
17 checks passed
@jphein
jphein deleted the fix/413-postgres-embedding-resolver branch September 11, 2026 03:13
jphein added a commit that referenced this pull request Sep 18, 2026
Step 4 did `grep … | head -1` per document, so only the FIRST line mentioning
a PR number contributed to that document's claimed state. The check answered
"does the first mention agree?" rather than "do all mentions agree?", and a
drifted claim appearing after a correct one was invisible.

Reproduced on the real repo before fixing: appending

    PR MemPalace#1377 is still open upstream.

to the end of README.md, with MemPalace#1377 MERGED upstream, left check-docs
reporting "✓ all 245 PR references match upstream state".

The control was confirmed reachable BEFORE its silence was trusted: the
appended line is in the instrument's own match set (line 477 of 5 matches),
while `head -1` selects line 30 — which mentions MemPalace#1377 and claims nothing.
A control that is present but never looked at produces "did not fire" for the
wrong reason, and that reads identically to "no drift".

Three defects, now one scan:

- Only the first line was read. Every matching line is now, with the
  multi-PR commentary skip applied per LINE rather than zeroing a whole
  document's claims.
- No right boundary. The claim scan used (#$n|/$n), so `#45` matched `#452`
  and `/45` matched `/459efab`, while the commentary scan beside it used
  [^0-9] — the two loops could read different lines and reach a conclusion
  neither line supported. One scan, one regex, anchored on a non-digit or
  end of line.
- State words matched as substrings. "opencode", "openai-compat" and
  "reopened" all contain "open" and were read as a claim of OPEN. Latent
  while one line per doc was examined; amplified the moment every line is.

Measured differentially over the whole repo with every PR stubbed MERGED:
7 findings before, 7 after — and five different ones in each direction.

  removed (false positives):  #45   read off a line about #452
                              #56   "OpenCode adapter smoke test"
                              #463  "openai-compat embedding"
                              MemPalace#1567 ".opencode/opencode.json"
                              MemPalace#2062 v3.8.0 sync line
  found (never examined):     #23   "PR #23 is still OPEN but"
                              #168  "#168 itself stays open"
                              MemPalace#665  "*Upstream:* [PR MemPalace#665] (OPEN)"
                              MemPalace#1087 "(OPEN)"
                              MemPalace#1094 "(open upstream, jp-authored)"

The unchanged total is the trap: a reader checking whether the count moved
would conclude nothing had.

Also removes both shellcheck errors in the file (SC1087 — `$n[` read as
array indexing) and adds none; the two remaining warnings are pre-existing.

Known limit, asserted in a test rather than left implicit: when one clean
line claims the true state and another claims a different one, drift cannot
be distinguished from history, and the PR is skipped. MemPalace#1024 in
FORK_CHANGELOG.md is exactly that shape — "pushed to the open MemPalace#1024 PR
branch (squash-merged upstream)" alongside an authoritative "(MERGED)" — and
is correct documentation of a MERGED PR. It is the only such pair in the
repo, which is why flagging disagreement was rejected.

Tests: 11 new in tests/test_check_docs_pr_state.py, driving the real script
over a fixture tree with a stubbed `gh` (no network, no shared API quota).
Four guards mutation-verified, each mutation asserting its target exists so a
mutation that fails to apply reports loudly instead of as "nothing to guard":

    restore head -1            -> 3 tests
    remove the word boundary   -> 2
    remove the commentary skip -> 1
    drop the right boundary    -> 1

Three of those tests were not evidence when first written and were rebuilt:
the harness sliced output between step headings while findings go to stderr
(so every "no finding" assertion passed vacuously); the commentary test
claimed a state the MERGED branch ignores; and the boundary test used a
number the check never queried. Each now fails under the old behaviour.

check-docs passes on itself. Full suite 7443 passed, 82 skipped.

Part of #516
Fixes #516
jphein added a commit that referenced this pull request Sep 18, 2026
Step 4 did `grep … | head -1` per document, so only the FIRST line mentioning
a PR number contributed to that document's claimed state. The check answered
"does the first mention agree?" rather than "do all mentions agree?", and a
drifted claim appearing after a correct one was invisible.

Reproduced on the real repo before fixing: appending

    PR MemPalace#1377 is still open upstream.

to the end of README.md, with MemPalace#1377 MERGED upstream, left check-docs
reporting "✓ all 245 PR references match upstream state".

The control was confirmed reachable BEFORE its silence was trusted: the
appended line is in the instrument's own match set (line 477 of 5 matches),
while `head -1` selects line 30 — which mentions MemPalace#1377 and claims nothing.
A control that is present but never looked at produces "did not fire" for the
wrong reason, and that reads identically to "no drift".

Three defects, now one scan:

- Only the first line was read. Every matching line is now, with the
  multi-PR commentary skip applied per LINE rather than zeroing a whole
  document's claims.
- No right boundary. The claim scan used (#$n|/$n), so `#45` matched `#452`
  and `/45` matched `/459efab`, while the commentary scan beside it used
  [^0-9] — the two loops could read different lines and reach a conclusion
  neither line supported. One scan, one regex, anchored on a non-digit or
  end of line.
- State words matched as substrings. "opencode", "openai-compat" and
  "reopened" all contain "open" and were read as a claim of OPEN. Latent
  while one line per doc was examined; amplified the moment every line is.

Measured differentially over the whole repo with every PR stubbed MERGED:
7 findings before, 7 after — and five different ones in each direction.

  removed (false positives):  #45   read off a line about #452
                              #56   "OpenCode adapter smoke test"
                              #463  "openai-compat embedding"
                              MemPalace#1567 ".opencode/opencode.json"
                              MemPalace#2062 v3.8.0 sync line
  found (never examined):     #23   "PR #23 is still OPEN but"
                              #168  "#168 itself stays open"
                              MemPalace#665  "*Upstream:* [PR MemPalace#665] (OPEN)"
                              MemPalace#1087 "(OPEN)"
                              MemPalace#1094 "(open upstream, jp-authored)"

The unchanged total is the trap: a reader checking whether the count moved
would conclude nothing had.

Also removes both shellcheck errors in the file (SC1087 — `$n[` read as
array indexing) and adds none; the two remaining warnings are pre-existing.

Known limit, asserted in a test rather than left implicit: when one clean
line claims the true state and another claims a different one, drift cannot
be distinguished from history, and the PR is skipped. MemPalace#1024 in
FORK_CHANGELOG.md is exactly that shape — "pushed to the open MemPalace#1024 PR
branch (squash-merged upstream)" alongside an authoritative "(MERGED)" — and
is correct documentation of a MERGED PR. It is the only such pair in the
repo, which is why flagging disagreement was rejected.

Tests: 11 new in tests/test_check_docs_pr_state.py, driving the real script
over a fixture tree with a stubbed `gh` (no network, no shared API quota).
Four guards mutation-verified, each mutation asserting its target exists so a
mutation that fails to apply reports loudly instead of as "nothing to guard":

    restore head -1            -> 3 tests
    remove the word boundary   -> 2
    remove the commentary skip -> 1
    drop the right boundary    -> 1

Three of those tests were not evidence when first written and were rebuilt:
the harness sliced output between step headings while findings go to stderr
(so every "no finding" assertion passed vacuously); the commentary test
claimed a state the MERGED branch ignores; and the boundary test used a
number the check never queried. Each now fails under the old behaviour.

check-docs passes on itself. Full suite 7443 passed, 82 skipped.

Part of #516
Fixes #516
jphein added a commit that referenced this pull request Sep 18, 2026
) (#520)

* fix(check-docs): examine every mention of a PR, not just the first

Step 4 did `grep … | head -1` per document, so only the FIRST line mentioning
a PR number contributed to that document's claimed state. The check answered
"does the first mention agree?" rather than "do all mentions agree?", and a
drifted claim appearing after a correct one was invisible.

Reproduced on the real repo before fixing: appending

    PR MemPalace#1377 is still open upstream.

to the end of README.md, with MemPalace#1377 MERGED upstream, left check-docs
reporting "✓ all 245 PR references match upstream state".

The control was confirmed reachable BEFORE its silence was trusted: the
appended line is in the instrument's own match set (line 477 of 5 matches),
while `head -1` selects line 30 — which mentions MemPalace#1377 and claims nothing.
A control that is present but never looked at produces "did not fire" for the
wrong reason, and that reads identically to "no drift".

Three defects, now one scan:

- Only the first line was read. Every matching line is now, with the
  multi-PR commentary skip applied per LINE rather than zeroing a whole
  document's claims.
- No right boundary. The claim scan used (#$n|/$n), so `#45` matched `#452`
  and `/45` matched `/459efab`, while the commentary scan beside it used
  [^0-9] — the two loops could read different lines and reach a conclusion
  neither line supported. One scan, one regex, anchored on a non-digit or
  end of line.
- State words matched as substrings. "opencode", "openai-compat" and
  "reopened" all contain "open" and were read as a claim of OPEN. Latent
  while one line per doc was examined; amplified the moment every line is.

Measured differentially over the whole repo with every PR stubbed MERGED:
7 findings before, 7 after — and five different ones in each direction.

  removed (false positives):  #45   read off a line about #452
                              #56   "OpenCode adapter smoke test"
                              #463  "openai-compat embedding"
                              MemPalace#1567 ".opencode/opencode.json"
                              MemPalace#2062 v3.8.0 sync line
  found (never examined):     #23   "PR #23 is still OPEN but"
                              #168  "#168 itself stays open"
                              MemPalace#665  "*Upstream:* [PR MemPalace#665] (OPEN)"
                              MemPalace#1087 "(OPEN)"
                              MemPalace#1094 "(open upstream, jp-authored)"

The unchanged total is the trap: a reader checking whether the count moved
would conclude nothing had.

Also removes both shellcheck errors in the file (SC1087 — `$n[` read as
array indexing) and adds none; the two remaining warnings are pre-existing.

Known limit, asserted in a test rather than left implicit: when one clean
line claims the true state and another claims a different one, drift cannot
be distinguished from history, and the PR is skipped. MemPalace#1024 in
FORK_CHANGELOG.md is exactly that shape — "pushed to the open MemPalace#1024 PR
branch (squash-merged upstream)" alongside an authoritative "(MERGED)" — and
is correct documentation of a MERGED PR. It is the only such pair in the
repo, which is why flagging disagreement was rejected.

Tests: 11 new in tests/test_check_docs_pr_state.py, driving the real script
over a fixture tree with a stubbed `gh` (no network, no shared API quota).
Four guards mutation-verified, each mutation asserting its target exists so a
mutation that fails to apply reports loudly instead of as "nothing to guard":

    restore head -1            -> 3 tests
    remove the word boundary   -> 2
    remove the commentary skip -> 1
    drop the right boundary    -> 1

Three of those tests were not evidence when first written and were rebuilt:
the harness sliced output between step headings while findings go to stderr
(so every "no finding" assertion passed vacuously); the commentary test
claimed a state the MERGED branch ignores; and the boundary test used a
number the check never queried. Each now fails under the old behaviour.

check-docs passes on itself. Full suite 7443 passed, 82 skipped.

Part of #516
Fixes #516

* fix(check-docs): a state WORD is not a state CLAIM; /pull/N, not /N

Follow-up on the same PR, applying nebula's measurement from the issue
thread. The first pass fixed `head -1` and matched state words as whole
words; that was not enough, and I could prove it only after running the
check with REAL upstream states.

Two corrections to my own verification first, because they are why this
was nearly missed:

  * My "no new false positives" run used a stub that returned a state for
    ONE pr number and nothing for the rest, so every other PR was skipped.
    "Clean" there proved nothing. With real `gh` the fix ADDED a warning.
  * The control line nebula cited (FORK_CHANGELOG.md L246) is blank in this
    tree — the measurement was taken on another branch. Found by content
    instead: it is L358 here, and L356 is a worse case nebula predicted but
    could not see.

Measured with real states, before this commit:

    NEW script: 2 warnings   (MemPalace#1377, #459)
    OLD script: 1 warning    (MemPalace#1377)

Both causes are the same defect from the other end. `head -1` decided
WHICH lines are read; this decides WHAT counts as a claim on a line.

  #459  README.md:297 and FORK_CHANGELOG.md:356 are the heading "purge /
        prune / mined share one open-and-refuse sequence", linking commit
        `459efab`. There is no #459 on either line — `/459` matched inside
        the COMMIT HASH, and "open-and-refuse" supplied a whole-word "open".
        A right boundary does not help: `/459` is followed by `e`.
  MemPalace#1377 FORK_CHANGELOG.md:81 was MY OWN changelog entry, quoting the
        control sentence verbatim. The entry documenting the defect
        reproduced it — the `self-quoting-retraction` shape from the #503
        spec, which is how #511 went green and then warned again once its
        own entry landed.

So, three rules now:

  * `/pull/$n`, never a bare `/$n`. A commit hash is not a PR reference.
  * A state word must appear in a CLAIM SHAPE — a parenthesised marker
    "(OPEN)", or a copula "is/was/stays/remains/now [still] open". Checked
    against all 13 cases in this repo: every real claim kept (#23 "is still
    OPEN", #168 "stays open", MemPalace#665/MemPalace#1087 "(OPEN)", MemPalace#1094 "(open upstream"),
    every prose case dropped ("open-and-refuse", "open the drawers",
    "opencode", "openai-compat", "reopened").
  * The entry for this change does not quote its own control sentence. It
    states a MERGED claim for MemPalace#1377, which is what MemPalace#1377 is.

Result with real `gh` on the repo itself: both the base script and this one
report zero PR-state warnings. The base's cleanliness is incidental — its
`head -1` happens to land on a line without a claim, and moved there only
because #509/#512/this entry changed the changelog. This one is clean for a
reason.

shellcheck findings 4 -> 2 (both remaining are pre-existing warnings; both
former ERRORS are gone).

Tests: 5 more (state word without a claim; nebula's "open the drawers" with
the PR alone on the line, since the real one is spared only incidentally by
a second PR sharing it; a commit hash is not a PR reference; a /pull/ URL
still counts; a parenthesised marker is still a claim). 16 total, and two
more mutations verified with the target-exists assertion:

    claim shape -> bare word   -> 3 tests
    /pull/N     -> /N          -> 2 tests

One test asserted something the check cannot do — a PR referenced only by
URL is never examined, because the number list is harvested from `#NNNN`
alone. Pre-existing and out of scope; the test now says so rather than
pretending to cover it.

Full suite 7448 passed, 82 skipped. check-docs passes on itself.

Part of #516

* docs(fork-changes): renumber to seq 157 after the #511/#522 rebase

Rebased onto 4c6a8d0. `--next-seq` re-run after the final fetch says 157;
taken from the tool rather than assumed. Generated artefacts re-rendered
from main's side, never hand-merged.

Part of #516
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.

2 participants