Skip to content

feat(mt#4765): Validate stored tracksTask associations against their text - #3496

Merged
edobry merged 7 commits into
mainfrom
task/mt-4765
Aug 31, 2026
Merged

feat(mt#4765): Validate stored tracksTask associations against their text#3496
edobry merged 7 commits into
mainfrom
task/mt-4765

Conversation

@minsky-ai

@minsky-ai minsky-ai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

ADR-012 §Implementation Follow-Ups → Child task 2 prescribes deriving associations.tracksTask
from content patterns and closes with a bullet that was never built:

Validate by comparing structured associations against body-text grep

mt#4448 shipped the derivation (the backfill, and every memory.create) without it. Because
extractTrackingTaskRefs prefers a stored association and never re-scans the text, a false
positive minted once is permanent — immune to every later improvement to the patterns. This
ships the missing validation.

Measured over the full corpus (1,343 memories, 182 carrying tracksTask, 198 stored refs):
9 refs across 9 records are false, every one the detector firing on prose about retirement
rather than a retirement clause.

Key changes

scripts/rederive-memory-associations.ts — dry-run by default; --execute requires a plan token
minted by the dry-run, aborts on drift, re-reads each record immediately before writing, and skips
any record whose tracksTask moved since the dry-run.
scripts/rederive-memory-associations-results.json — the committed measurement snapshot.

Both derivations come from the shipped extractor. A new skipQuotationElision option on
extractTrackingTaskRefs (packages/domain/src/memory/staleness.ts) reproduces the pre-mt#4454
behaviour, so the script can compare:

  • derivedNow — what the extractor produces today (quotation-elided). Ground truth.
  • derivedRaw — what it produced when the stored associations were minted.

A ref in derivedRaw but not derivedNow was minted from a quoted clause: the old patterns saw
it, the corrected ones do not. That is an exact statement about the write path, and it is the only
bucket this script corrects. The option is documented as archaeology with one caller and barred
from read/write paths — passing it reproduces a defect — and is an options bag rather than a second
exported function so it cannot be mistaken for a peer entry point.

Two other design points worth review:

  1. Per REF, not per record. mem#1208 carries one true ref beside one false one.
  2. not-derivable is deliberately NOT actionable. A derived value and an author-DECLARED value
    are byte-identical once stored. Measured support: 5 of the 7 not-derivable write-time records
    have updated_at == created_at, and the write path derives only when the caller supplied
    nothing — so those were explicitly authored. Auto-correcting that bucket would delete real
    associations.

History: this PR was reworked mid-review

mt#4454 shipped the ADR-024 Rung 1 quotation prefilter while this PR was in review, which
invalidated the original design (a local approximation of that pass, written when the extractor had
none). Reworking onto the shipped primitive raised the finding count from 6 to 9 — the local copy
only elided code spans; the real one also covers part (b), prose-quoted spans.

That rework also surfaced a defect in the freshly-merged primitive itself (it blanked to spaces, so
\s+ matched across elided holes and manufactured matches). Fixed separately in mt#4792 /
PR #3502, merged and deployed; this PR sits on top of it.

Testing

Execution evidence:

$ bun test --preload ./tests/setup.ts scripts/rederive-memory-associations.test.ts
 10 pass   0 fail   12 expect() calls

$ bun test --preload ./tests/setup.ts packages/domain/src/memory packages/domain/src/text
 205 pass   0 fail   381 expect() calls   (12 files)

SC1 / AT1 — the write-time-minted population (16 records; boundary is the derivation commit
d10d1243a, 2026-08-24T20:56:48Z, not the calendar date):

mem#1237  mt#4525[quoted-only]     mem#1315  mt#4677[grounded]
mem#1239  mt#4529[grounded]        mem#1316  mt#4659[grounded]
mem#1243  mt#4449[not-derivable]   mem#1317  mt#4669[grounded]
mem#1249  mt#4473[grounded]        mem#1325  mt#4701[not-derivable]
mem#1253  mt#4548[not-derivable]   mem#1326  mt#4301[not-derivable]
mem#1267  mt#4347[not-derivable]   mem#1340  mt#1541[quoted-only]
mem#1302  mt#4658[grounded]        mem#1344  mt#4757[not-derivable]
mem#1308  mt#4666[not-derivable]   mem#1311  mt#4655[quoted-only]

3 of 16 false (mem#1237 joined after the rework). Excluding the 5 provably-declared leaves 9–11
genuinely derived, so 27–33%.

SC2 — a census, not a sample: all 182 records carrying tracksTask were classified, so the
sample-size question is moot. Backfilled population: 6 false of 166 = 3.6%.

SC3 / SC5 — the mechanism, this diff. SC5 is now demonstrated rather than promised: mt#4454 DID
land mid-flight and the mechanism was re-pointed at it without changing the classification contract.

SC6 / AT3 — discrimination control. mem#315 carries both verdicts through the same code
path in the same record: mt#1034[grounded] mt#1503[quoted-only], keeping its genuine tracking
task while dropping the quoted one.

AT2 — mem#1340 classified FALSE by the mechanism, not by hand-annotation: it appears in the
committed plan, and a unit test asserts the verdict from its verbatim stored text.

SC4a / AT4 — dry-run half, plan token 4200dcaa2378b6e2, measured 2026-08-31T01:12Z:

mem#1340  ['mt#1541']            -> []
mem#1311  ['mt#4655']            -> []
mem#1237  ['mt#4525']            -> []
mem#1208  ['mt#2056','mt#4454']  -> ['mt#4454']
mem#484   ['mt#2056']            -> []
mem#361   ['mt#1637']            -> []
mem#315   ['mt#1034','mt#1503']  -> ['mt#1034']
mem#199   ['mt#1034','mt#1503']  -> ['mt#1034']
mem#82    ['mt#1034','mt#1503']  -> ['mt#1034']

SC4b — the run against prod is NOT in this PR. Writing the memories table is a shared/
production state change held for explicit principal authorization; it is a data operation, and
merging this PR changes no memory record. SC4b remains owed and the task does not close until it
runs or is waived.

Negative control — the two-mode derivation: set skipQuotationElision: false so both derivations
are identical, and the comparison goes inert:

[control]   quoted-only (ACTIONABLE):  0     Records to correct: 0
[restored]  quoted-only (ACTIONABLE):  9     Records to correct: 9

Negative control — entrypoint guard: removed if (import.meta.main) and imported the module; it
opened Postgres and began a full scan. With the guard, only the pure exports.

All nine read by hand, not counted (mem#1208's discipline — a rate bounds volume and says
nothing about correctness):

Record Ref The text that fired
mem#1340 mt#1541 - `Retire when mt#1541 ships.` → HIT — a discrimination-control table documenting this very extractor, matched as its own fixture
mem#1311 mt#4655 `## Subsumed by mt#4655` — a code span quoting another task's spec heading
mem#1208 mt#2056 `retire when mt#2056 ships` — quoted while narrating it as a known false positive
mem#484 mt#2056 "Bridge memory 70ba7f79 had a budget criterion: "retire when mt#2056 ships AND…"" — quoting ANOTHER memory's criterion. This is the record mem#1208 documents as canonical, and the pre-rework classifier missed it
mem#1237 mt#4525 "The condition this memory set for itself ("retires when mt#4525…") is met" — quoting its own superseded condition
mem#361 mt#1637 "This memory's budget said "retire when mt#1637 ships."" — inside a ## RETIRED historical record
mem#82 / #199 / #315 mt#1503 Example: `Budget: retire when ≥3 invocations in 14d; tracking task: mt#1503.` — the memory teaching the budget format, its own worked example read as a declaration

A sub-shape worth naming: three of the nine are memories quoting their own past retirement
condition
while recording that it has been discharged. Writing down that a budget fired re-mints
the association the budget was about — so this class recurs in exactly the records that are best
maintained.

Bound on every rate above: these are lower bounds. The classifier detects the quoted-only
shape only — a false positive whose clause is unquoted classifies grounded and is invisible
here.

Typecheck clean (8 projects); lint clean (4,214 files). Both against the session workspace.

Deploy verification

This PR touches one deploy-surface file — packages/domain/src/memory/staleness.ts, for the
skipQuotationElision option. The original diff was scripts-only; the option arrived with the
mt#4792 rework, which is why this section postdates the first review round.

After merge I will run mcp__minsky__deployment_wait-for-latest for minsky-mcp with
notBefore = the merge timestamp and expectCommitSha = the merge SHA, read buildIdentity, and
— since that service is image-source and returns indeterminate by construction — correlate the
Deploy MCP workflow run against the merge SHA and assert the /health body's service field
rather than the status code. Deploy-SUCCESS alone is not the completion signal.

The change is additive and default-off: skipQuotationElision is undefined on every existing
caller, so the deployed read and write paths behave exactly as they do today.

edobry added 2 commits August 30, 2026 16:13
…against their own text

ADR-012 child task 2's last bullet — "Validate by comparing structured associations against
body-text grep" — was never built. mt#4448 shipped the derivation without it, and
extractTrackingTaskRefs prefers a stored association and never re-scans, so a text false
positive minted once is permanent.

Adds scripts/rederive-memory-associations.ts: per-REF classification (grounded / quoted-only /
not-derivable), dry-run by default, tasks_bulk-edit-style plan token on --execute.

- Per-ref, not per-record: a record can hold one true ref and one false one (mem#1208).
- not-derivable is deliberately NOT actionable — a derived value and an author-DECLARED value
  are byte-identical once stored, so auto-correcting that bucket would delete real associations.
- Pages the whole corpus and asserts coverage against count(). MemoryService.list() caps at 500;
  the first live run scanned 500 of 1343 and reported it as a total.
- Elision fills with U+00B7, not spaces: blanking to whitespace lets the extractor's own \s+ span
  the removed text and manufacture a match the raw text never produced.
@minsky-ai minsky-ai Bot added the authorship/co-authored Co-authored by human and AI agent label Aug 30, 2026
@minsky-reviewer

minsky-reviewer Bot commented Aug 30, 2026

Copy link
Copy Markdown

Minsky Reviewer Status

Verdict: APPROVED — no blocking findings
Review: View review
Model: openai/gpt-5 | Tokens: 225K prompt, 6K completion | Duration: 129s
Mode: normal

Commands

  • /review — request a fresh review

@minsky-reviewer minsky-reviewer Bot 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.

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2


Solid, focused implementation with helpful tests, but three issues block merge. 1) The script unconditionally executes main() on import, so tests that import its exports will initialize config, connect to Postgres, and may call process.exit, making unit tests flaky or impossible — gate the entrypoint with if (import.meta.main) or split CLI from library. 2) The completeness check compares a paged list() result to count() and throws on mismatch; in a live system this can yield false failures due to drift between calls — use a consistent snapshot, cursor-based iteration, or a safer tolerance strategy. 3) The apply phase reconstructs the entire associations object from a possibly stale snapshot and writes it back, risking silent loss of unrelated association keys changed since the dry-run — re-fetch current associations per record at apply and patch only tracksTask. Non-blocking: minor arg-parsing brittleness. Adoption sweep indicates new exports are used locally. No docs impact. Address the blockers and this will be ready.

Findings

  • [BLOCKING] scripts/rederive-memory-associations.ts:379 — Module invokes main() on import — tests importing this file will execute the CLI (DB calls, process.exit)
    At the end of scripts/rederive-memory-associations.ts the code unconditionally calls main():
main().catch((err) => {
  console.error("Fatal error:", err);
  process.exit(1);
});

Because the test file (scripts/rederive-memory-associations.test.ts:1) imports from this module, the main() function will run at import time, attempting to initialize configuration, connect to Postgres, and potentially exit the process. This makes the module non-importable for unit tests and couples tests to environment/DB availability. Standard practice is to guard the entrypoint with if (import.meta.main) { main().catch(...); } (or split CLI from library), so exported pure helpers can be imported safely without side effects. Please gate the entrypoint to avoid executing the CLI during tests or when used as a library.

  • [BLOCKING] scripts/rederive-memory-associations.ts:222 — Flaky strict count check — corpus can change between count() and paged list() reads, causing false throw
    listEveryMemory() asserts out.length !== total to detect partial scans. In a live system the memories table can mutate between page fetches and the count() call (or between pages), yielding legitimate mismatches that make the script abort nondeterministically. This turns a safety check into an availability hazard. Remedies:

  • Take the count() snapshot BEFORE paging and, if possible, within a transaction/consistent read; OR

  • Tolerate small drift by re-reading when mismatched (double-check with a fresh pass), or assert out.length >= totalAtStart and warn on drift; OR

  • Prefer a paged count-less iteration using a stable cursor/ordering (e.g., id > lastId) that guarantees completeness without relying on a separate count().

As written, executions can fail spuriously even when pagination is correct.

  • [BLOCKING] scripts/rederive-memory-associations.ts:337 — Risk of dropping unrelated association keys on update — builds associations from stale snapshot, not current record
    In the apply loop, associations is initialized from the in-memory withTracks snapshot:
const source = withTracks.find((m) => m.id === entry.id);
const associations = { ...(source?.associations ?? {}) } as Record<string, string[]>;
...
await memoryService.update(entry.id, { associations });

If the record’s associations changed between the dry-run and execute (even if the plan token still matches because before/after are unchanged), or if withTracks is stale/missing the latest keys, this write replaces the entire associations object with a partial copy, silently dropping unrelated association keys/values. This violates the script’s own “preserve every other association key” intent and can corrupt data. Fix by re-fetching the current record (or at minimum its associations) at apply time and patching only tracksTask within that object, or by providing a partial-update API that targets just TRACKS_TASK_ASSOCIATION.

  • [NON-BLOCKING] scripts/rederive-memory-associations.ts:311 — CLI arg parsing computes tokenArg even when --token is absent; could pick up argv[0] accidentally
    In main(), const tokenArg = argv[argv.indexOf("--token") + 1]; is evaluated unconditionally. When --token is not present, indexOf returns -1 so + 1 reads argv[0]. The subsequent guard argv.includes("--token") ? tokenArg : undefined prevents using it, but this pattern is brittle and easy to regress. Suggest computing the index only when present:
let suppliedToken: string | undefined;
const i = argv.indexOf("--token");
if (i !== -1) suppliedToken = argv[i + 1];

This avoids accidental coupling to argv[0] if the guard is refactored later.

Adoption sweep

Symbol Kind Consumers found Classification Notes
scripts/rederive-memory-associations.elideQuotations function scripts/rederive-memory-associations.test.ts:6 — imported and used in tests, scripts/rederive-memory-associations.ts:128 — internal use via classifyRecord Adopted Exported from a script module; primary consumer is the colocated test. No external/public API exposure.
scripts/rederive-memory-associations.classifyRecord function scripts/rederive-memory-associations.test.ts:6 — imported and exercised in multiple tests, scripts/rederive-memory-associations.ts:175 — used to classify each memory during planning Adopted New export consumed by its test and by the script itself.
scripts/rederive-memory-associations.correctedRefs function scripts/rederive-memory-associations.test.ts:7 — imported and asserted, scripts/rederive-memory-associations.ts:209 — used to build the plan Adopted Internal script export; primary consumer is the colocated test.

Documentation impact

  • no-update-needed — This PR adds an internal corrective script and tests; it does not change exported domain behavior, public CLI, or docs. No existing docs were updated in this PR and none appear to describe this internal script. I did not find docs referencing rederive-memory-associations or its contracts.

@minsky-reviewer minsky-reviewer Bot 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.

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2


Round 2 verification: the prior round’s three BLOCKING findings remain unresolved in this commit. 1) The script still unconditionally invokes main() on import, so tests importing its exports will execute the CLI (DB init and process.exit) — gate with if (import.meta.main) or split CLI/library. 2) The pagination completeness guard strictly compares paged list size to count(); under live drift this can throw spuriously and turn a safety check into availability risk — use a consistent snapshot or a cursor-based iteration, or relax with a verification pass. 3) The apply phase reconstructs the full associations object from a possibly-stale pre-scan (withTracks) and writes it back, risking loss of unrelated association keys modified since dry-run — re-fetch current associations per record and patch only tracksTask (or expose a targeted update). Non-blocking: minor arg-parsing brittleness for --token computation. Spec-wise, SC3/SC5 are met by the script and tests; SC4 is intentionally deferred and marked Not Met pending principal authorization. Address the blockers and this will be ready to approve.

Findings

  • [BLOCKING] scripts/rederive-memory-associations.ts:372 — Module invokes main() on import — tests importing this file will execute the CLI (DB calls, process.exit)
    At the end of scripts/rederive-memory-associations.ts the code unconditionally calls main():
main().catch((err) => {
  console.error("Fatal error:", err);
  process.exit(1);
});

The test file (scripts/rederive-memory-associations.test.ts:1) imports named exports from this module, which will execute main() at import time, attempting to initialize configuration, connect to Postgres, and potentially exit the process. This couples tests to environment/DB availability and makes the module non-importable for unit tests. Guard the entrypoint with if (import.meta.main) { main().catch(...); } (or split CLI from library) so exported pure helpers can be imported safely without side effects.

  • [BLOCKING] scripts/rederive-memory-associations.ts:222 — Strict equality check between paged list() size and count() can spuriously throw on live drift, turning a safety check into availability risk
    listEveryMemory() pages with a fixed PAGE and then asserts out.length !== total to detect partial scans, throwing when they differ:
const total = await service.count();
if (out.length !== total) {
  throw new Error(`Scan covered ${out.length} of ${total} ...`);
}

In a live system rows can be added/removed between the count() and the paginated reads (or between pages), legitimately producing mismatches and aborting runs nondeterministically. Remedies include: (a) obtain a consistent snapshot (transaction/AS OF SYSTEM TIME-style read) covering both count and pages; or (b) use a cursor/monotonic key iteration (e.g., id > lastId ORDER BY id LIMIT PAGE) to guarantee completeness without count(); or (c) tolerate drift by warning and optionally re-running a verification pass rather than throwing. As written, correct pagination under table churn can still fail spuriously.

  • [BLOCKING] scripts/rederive-memory-associations.ts:337 — --execute applies updates using a possibly-stale in-memory snapshot, risking loss of unrelated association keys
    During apply, the code builds the associations object from the earlier withTracks array (collected before planning):
const source = withTracks.find((m) => m.id === entry.id);
const associations = { ...(source?.associations ?? {}) } as Record<string, string[]>;
if (entry.after.length > 0) {
  associations[TRACKS_TASK_ASSOCIATION] = entry.after;
} else {
  delete associations[TRACKS_TASK_ASSOCIATION];
}
await memoryService.update(entry.id, { associations });

If the record’s associations changed between the dry-run and execute (even when the plan token still matches), or if other association keys were added/modified out-of-band, this write replaces the entire associations object with a partial, stale copy — silently dropping unrelated keys/values. Fix by re-fetching the current record (or its associations) at apply time and patching only tracksTask within that object, or by using a partial-update API that targets just TRACKS_TASK_ASSOCIATION.

  • [NON-BLOCKING] scripts/rederive-memory-associations.ts:279 — Arg parsing computes tokenArg even when --token is absent; brittle pattern
    const tokenArg = argv[argv.indexOf("--token") + 1]; is evaluated unconditionally. When --token is not present, indexOf returns -1 so + 1 reads argv[0]. The subsequent guard argv.includes("--token") ? tokenArg : undefined prevents use today, but the pattern is fragile and easy to regress. Prefer:
let suppliedToken: string | undefined;
const i = argv.indexOf("--token");
if (i !== -1) suppliedToken = argv[i + 1];

This avoids accidental coupling to argv[0] if the guard is refactored later.

Spec verification

Criterion Status Evidence
SC1 — The write-time-minted population (16 records, created_at >= 2026-08-24T20:56:48Z) is classified by hand with per-record ref-vs-text, and a timestamped rate is reported. Ambiguity-band records are listed separately. Unverifiable This criterion depends on a live, out-of-repo execution and dataset. The PR body includes a run log, but no repo artifact carries the per-record table. As a reviewer I cannot verify production runs from the diff alone.
SC2 — The same classification is run over a random sample large enough to bound the backfilled population’s rate, and the sample size basis is stated. Unverifiable Also depends on live corpus classification. The PR body claims a full-census run, but no in-repo artifact asserts it; cannot be verified from code alone.
SC3 — A re-derivation path exists: recompute tracksTask from current text under current patterns and report the diff. Dry-run by default; --execute gated; wrapped in this task. Met scripts/rederive-memory-associations.ts defines pure helpers elideQuotations, classifyRecord, correctedRefs, and planning/output logic, plus a CLI shell that prints a dry-run plan and supports --execute with a plan-token gate. See scripts/rederive-memory-associations.ts:1-170, 209-383 and tests in scripts/rederive-memory-associations.test.ts.
SC4 — Records whose association is confirmed false are corrected, with before/after recorded. Dry-run-then-token discipline or equivalent recorded diff. Not Met The script implements --execute with a plan-token check, but the PR explicitly states the executed half is deliberately not in this PR (held for principal authorization). No executed before/after diff is included; only dry-run is shown. Either update the task spec to record this deferral or include the executed diff.
SC5 — The re-derivation is repeatable after mt#4454 lands; the mechanism is not a one-shot script that must be rewritten then. Met The quotation/elision is factored as a single exported function (elideQuotations) with a documented contract, and the classification logic calls it. Swapping in a shared primitive later would not change the classification shape. See scripts/rederive-memory-associations.ts:33-102 and its unit tests.
SC6 — Negative control: a record with a genuine, correctly-derived association is left untouched by the corrective pass; mem#1340 (false) and a confirmed-true record produce opposite verdicts via the same path. Met Unit tests assert discrimination: mem#1340-shaped input classifies quoted-only while an unquoted clause classifies grounded, and correctedRefs drops only quoted refs. See scripts/rederive-memory-associations.test.ts:55-123.

Documentation impact

  • no-update-needed — Adds an internal corrective script and tests only. No changes to exported domain behavior, public CLI surface, or docs paths. I searched this PR’s diff for docs updates and for references to this script; none present. Behaviour remains internal to scripts/.

…e-free, re-read before write

All three BLOCKING findings were legitimate; no false positives to dispute.

- main() ran on IMPORT, so the test file's import opened Postgres and started the scan.
  Verified by negative control: guard removed -> "Persistence provider created" + a full scan on
  a bare import; guard restored -> only the four pure exports.
- The coverage check used `!==` against a count taken AFTER paging, so a record created mid-scan
  false-threw. Count is now taken FIRST and compared with `<`: a concurrent insert can only make
  the scan exceed the floor, a concurrent delete only lowers the true total, and the case worth
  catching — a short read — still throws.
- The execute path built the associations map from the scan-time snapshot, so a key added by a
  concurrent writer would have been silently reverted (update REPLACES the map). Now re-reads
  each record immediately before writing and SKIPS it if tracksTask moved since the dry-run.
  The plan token guards the record SET; this guards each record's content at write time.
- Non-blocking: --token read argv[0] when the flag was absent (indexOf -1 + 1). Guarded.

Dry-run re-verified: 1343 scanned, same 6 corrections, token unchanged.

@minsky-reviewer minsky-reviewer Bot 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.

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2


Re-verification against the latest commit shows the prior BLOCKING issues were addressed: main() is now guarded behind import.meta.main, the coverage assertion is made race-safe by taking count() first and comparing with '<', and the write path re-reads live data to avoid clobbering concurrent association keys. I found no new defects introduced by this fix commit. However, per the current task spec, SC4 (executed corrective pass with recorded before/after) remains unmet in this PR — the body explicitly holds execution for principal authorization. Either amend the spec to defer SC4 or include the executed diff; until then, this PR does not satisfy all success criteria. Other criteria are met or unverifiable from code alone. Overall, the implementation quality looks solid; merge is blocked solely on the SC4 gap.

Findings

  • [BLOCKING] (review summary):1 — Reviewer concluded REQUEST_CHANGES but emitted no structured findings
    Synthesized by the empty-findings coherence recovery pass (mt#2685): the reviewer model called conclude_review with event=REQUEST_CHANGES but zero submit_finding calls, so the structured findings channel was empty even though the conclusion summary describes blocking issue(s) in prose. Original conclusion summary:

Re-verification against the latest commit shows the prior BLOCKING issues were addressed: main() is now guarded behind import.meta.main, the coverage assertion is made race-safe by taking count() first and comparing with '<', and the write path re-reads live data to avoid clobbering concurrent association keys. I found no new defects introduced by this fix commit. However, per the current task spec, SC4 (executed corrective pass with recorded before/after) remains unmet in this PR — the body explicitly holds execution for principal authorization. Either amend the spec to defer SC4 or include the executed diff; until then, this PR does not satisfy all success criteria. Other criteria are met or unverifiable from code alone. Overall, the implementation quality looks solid; merge is blocked solely on the SC4 gap.

Spec verification

Criterion Status Evidence
SC1 — The write-time-minted population (16 records, created_at >= 2026-08-24T20:56:48Z) is classified by hand with per-record ref-vs-text, and a measurement rate is reported with timestamp. Unverifiable The PR body includes a per-record listing and rates, but these artifacts are live-run outputs outside the repo. No in-repo file carries this table. Without re-running against prod, this is not verifiable from the diff alone.
SC2 — The same classification over a sufficient random sample of the 166 backfilled records, with the sample size and basis stated. Unverifiable The PR description claims a full-census run over all 182 memories with tracksTask and reports 4 false of 166 backfilled. This evidence lives outside the repo; no artifact in the diff can be used to independently verify. Marking Unverifiable from code alone.
SC3 — A re-derivation path exists (dry-run by default, --execute gated, wrapped in this task). Met scripts/rederive-memory-associations.ts:20-34 (usage doc), 178-214 (planToken), 222-309 (main: dry-run summary, --json), 311-416 (execute path gated by --execute and --token). The script implements classification, planning, and gated execution.
SC4 — Records confirmed false are corrected, with before/after recorded; dry-run-then-token discipline or equivalent recorded diff. Not Met The code supports execution with token discipline (scripts/rederive-memory-associations.ts:222-416), but the PR explicitly states the corrective pass is NOT RUN in this PR (held for principal authorization). There is no executed before/after artifact in-repo. Either update the task spec to defer SC4 or add the executed diff per AT4.
SC5 — The re-derivation is repeatable after mt#4454 lands (mechanism survives a pattern change; not a one-shot script). Met The quotation pass is implemented as a single exported function with a documented contract (elideQuotations at scripts/rederive-memory-associations.ts:40-93), and classification composes it without depending on specific detector internals. Swapping in a shared primitive later does not change classification wiring.
SC6 — Negative control: a confirmed-true record remains untouched by the corrective pass while a false one is corrected, through the same path. Unverifiable This requires executing the corrective pass against specific live records (e.g., mem#315 vs mem#1340). The PR code supports this, but no executed, in-repo artifact demonstrates the discrimination; the PR body’s claims are out-of-repo execution evidence.

Adoption sweep

Symbol Kind Consumers found Classification Notes
scripts/rederive-memory-associations.elideQuotations function Missing consumers Exported from a script for unit testing and potential reuse; no explicit consumers found in this diff. Not required by the spec to be wired elsewhere.
scripts/rederive-memory-associations.classifyRecord function Missing consumers Exported for testing and potential reuse; no in-repo consumers identified in this diff. The task spec does not require wiring it elsewhere.

Recommendation: file a follow-up adoption task to wire 2 missing consumers.

Documentation impact

  • no-update-needed — This PR adds a maintenance script and internal helpers without changing any public API behavior or documented user-facing commands. No docs under docs/ are referenced or invalidated by this code; the execution evidence and measurement tables mentioned in the PR body are out-of-repo artifacts.

…an in-repo artifact

R3 confirmed all three prior BLOCKING findings fixed and found no new defects. The remaining
blocker was a scope mismatch, not a code defect: SC4 as written conflated the corrective CODE
with the ACT of mutating production data, so the PR read as Not Met against its own spec.

Taking the reviewer's option (a): SC4 is split in the task spec into SC4a (the token-gated,
drift-aborting, re-read-before-write corrective path — verifiable from this diff) and SC4b (the
run against prod, gated on explicit principal authorization). No `[sc4-deferred: mt#NNNN]` marker,
because the gate is a person rather than a task and such a marker would name a task that should
not exist. SC4b remains owed; the task does not close until it is run or waived.

Also addresses the reviewer's three "Unverifiable" spec verdicts, which were fair — the SC1/SC2
evidence lived only in live-run output with no in-repo artifact. The --json snapshot is now
committed, and carries `measuredAt` + `scanned` so a later reader can tell what it describes:
the corpus moves under this script, and a bare tally with no date cannot be re-derived.

@minsky-reviewer minsky-reviewer Bot 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.

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2


All prior BLOCKING findings are addressed. The new script implements dry-run-first re-derivation with census pagination and a hard short-read guard, classification that only drops quoted-only refs, a deterministic plan token, and per-record re-read/skip-on-drift before writes, satisfying SC3 and SC4a. The committed JSON artifact records the measurement timestamp and plan. No new critical defects were introduced by the fixes. SC4b is intentionally out-of-PR scope and Unverifiable. Documentation impact is none. Approving this PR.

Spec verification

Criterion Status Evidence
SC1 — The write-time-minted population (16 records, boundary = 2026-08-24T20:56:48Z) is classified by hand with a per-record ref-vs-text table and a stamped measurement time. Met PR description includes the per-record table and counts; the committed artifact scripts/rederive-memory-associations-results.json carries the run’s timestamp ("measuredAt": "2026-08-30T23:33:54.134Z") and plan reflecting those corrections. (File: scripts/rederive-memory-associations-results.json:1-60).
SC2 — The same classification is run over the backfilled population (or a sample large enough to bound the rate), and the sample size/basis is stated. Met PR description states a full census over all 182 memories carrying tracksTask (no sampling needed) and reports 4 false of 166 backfilled (2.4%). This census basis is also reflected in the tally in scripts/rederive-memory-associations-results.json (198 refs classified across 182 memories).
SC3 — A re-derivation path exists, dry-run by default, --execute gated, wrapped in this task. Met scripts/rederive-memory-associations.ts implements the mechanism: dry-run default with console summary/--json output and an --execute path that applies the plan. See main() option parsing and output (scripts/rederive-memory-associations.ts:238-356) and execution branch (scripts/rederive-memory-associations.ts:358-435).
SC4a — Corrective path (code) exists: --execute gated, requires a plan token minted by the dry-run, aborts on drift, re-reads each record immediately before writing, and skips any record whose tracksTask moved since the dry-run. Met - Plan token minted deterministically by planToken (scripts/rederive-memory-associations.ts:153-169) and required under --execute (scripts/rederive-memory-associations.ts:377-386).
  • Drift handling: token mismatch aborts (scripts/rederive-memory-associations.ts:377-386); per-record re-read and skip when tracksTask differs from before (scripts/rederive-memory-associations.ts:403-417). |
    | SC4b — The corrective pass is actually run against prod and before/after recorded (DATA operation, not in PR). | Unverifiable | By spec amendment, SC4b is explicitly out of PR scope and gated on principal authorization. This artifact lives outside the repo; no in-repo evidence can verify it here. |
    | SC5 — The re-derivation is repeatable after mt#4454 lands (not a one-shot script coupled to current patterns). | Met | elideQuotations is a local, documented elider (scripts/rederive-memory-associations.ts:35-88) with a contract designed to be replaced by the shared primitive later; classification logic (classifyRecord) composes around extractTrackingTaskRefs and the elider, so the mechanism remains valid after pattern changes (scripts/rederive-memory-associations.ts:109-147). |
    | SC6 — Negative control: a genuine, correctly-derived association is left untouched by the corrective pass. | Met | correctedRefs only drops refs with verdict quoted-only and preserves order (scripts/rederive-memory-associations.ts:149-152). Execution branch preserves other association keys and only mutates tracksTask to the after list (scripts/rederive-memory-associations.ts:419-433), ensuring grounded refs remain. |

Documentation impact

  • no-update-needed — This PR adds an internal maintenance script (scripts/rederive-memory-associations.ts) and commits a one-off measurement artifact JSON for traceability. No public API, CLI surface, or documented user behavior changed. No docs in docs/ reference this script or its output; no existing documentation is invalidated.

edobry added 3 commits August 30, 2026 20:18
… local approximation

mt#4792 merged, so the extractor now elides quotations correctly. This reworks the classifier onto
it instead of around it, and the count rose from 6 false positives to 9.

The script carried a LOCAL approximation of the quotation pass, written when the extractor had
none. Both derivations now come from the extractor itself:

  derivedNow — what it produces today (quotation-elided). Ground truth.
  derivedRaw — what it produced BEFORE mt#4454, via a new `skipQuotationElision` option.
               This is what actually MINTED the stored associations being audited.

A ref in derivedRaw but not derivedNow was minted from a quoted clause. That is now an exact
statement about the write path rather than an approximation of it, which is why it finds more:
the local copy only elided code spans, and the shipped primitive also covers ADR-024 Rung 1
part (b), prose-quoted spans.

The three new findings are all prose quotes, all hand-verified, and include mem#484 — the record
mem#1208 documents as the canonical false positive of this class, which the old classifier missed.
Three of the nine are memories quoting their OWN past retirement condition while recording that it
fired; writing down that a budget was discharged re-mints the association it was about.

`skipQuotationElision` is documented as archaeology with one caller and barred from read/write
paths — passing it reproduces a defect. An options bag, not a second exported entry point.

@minsky-reviewer minsky-reviewer Bot 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.

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2


Re-verification complete. The prior BLOCKING issues (unguarded entrypoint, inadequate coverage check against count-after-paging, and apply-path rebuilding associations from a stale snapshot) are all addressed: main() is guarded by import.meta.main, the scan now counts first and asserts out.length < floor with paging, and the write path re-reads each record, skips on drift, and preserves other association keys while only updating tracksTask. The extractor gained an optional skipQuotationElision option with clear “archaeology only” docs; default behavior is unchanged for existing callers. The re-derivation script derives both ways from the shipped extractor, emits a dry-run plan with a stable token, and enforces token + drift checks on --execute. New tests cover discrimination and token stability. I find no new critical defects introduced by these changes. Verdict: APPROVE.

Spec verification

Criterion Status Evidence
SC1 — The write-time-minted population (16 records, created_at >= 2026-08-24T20:56:48Z) is classified by hand with per-record ref-vs-text table and a timestamped rate. Met scripts/rederive-memory-associations-results.json:1-84 — committed snapshot with measuredAt (2026-08-31T01:12:14.894Z), tally and a per-record plan listing including mem#1340, mem#1311, mem#1237. Tests in scripts/rederive-memory-associations.test.ts use verbatim fixtures for mem#1340 and the mixed true/false shape (mem#1208).
SC2 — The same classification run over a sample of the 166 backfilled records large enough to bound their rate; sample size stated. Met scripts/rederive-memory-associations.ts:169-230 — the classifier runs across the entire corpus carrying tracksTask (census, not a sample). scripts/rederive-memory-associations-results.json:1-84 carries the corpus-wide tally (scanned: 1343; carrying: 182; quoted-only: 9).
SC3 — A re-derivation path exists: recompute tracksTask from current text/patterns and report the diff. Dry-run by default, --execute gated, wrapped in this task. Met scripts/rederive-memory-associations.ts:1-392 — CLI script implements classification and plan; dry-run default with --verbose/--json; --execute path applies with re-read and association preservation. Uses extractTrackingTaskRefs twice (current and skipQuotationElision) to derive both ways.
SC4 — Records whose association is confirmed false are corrected, with before/after recorded. Amended: SC4a (code, in PR) and SC4b (data, not in PR). Met SC4a: scripts/rederive-memory-associations.ts:254-392 — --execute requires a plan token (planToken at :137-156), aborts on token mismatch (:301-313), re-reads each record and skips on drift (:330-352), preserves other association keys and applies corrections (:356-377). Dry-run output shape provided via --json and console summary. SC4b is expressly out of PR per spec; see SC4b entry below.
SC4b (data, NOT in any PR) — the pass is actually run against prod and the before/after set recorded in ## Findings. Gated on explicit principal authorization. Unverifiable By spec, SC4b is a production run (no PR performs it). No repo artifact can verify it. The committed plan snapshot (scripts/rederive-memory-associations-results.json) records a dry-run diff and token, but the actual mutation is gated on principal authorization outside this diff.
SC5 — The re-derivation is repeatable after mt#4454 lands; not a one-shot script that must be rewritten then. Met packages/domain/src/memory/staleness.ts:241-297 — adds an options bag ExtractTrackingTaskRefsOptions and supports skipQuotationElision for archaeology only; scripts/rederive-memory-associations.ts derives both ways from the shipped extractor (current elided vs raw) ensuring future pattern changes are naturally incorporated without rewriting the script.
SC6 — Negative control: a record with a genuine, correctly-derived association is left untouched by the corrective pass; discriminate within the same record/path. Met scripts/rederive-memory-associations.test.ts:23-50 — tests assert mem#1340-style quoted-only vs grounded discrimination; :52-66 asserts a genuine unquoted retirement clause classifies grounded; :68-84 asserts one record with one quoted and one grounded ref, with correctedRefs retaining the grounded only.

Adoption sweep

Symbol Kind Consumers found Classification Notes
ExtractTrackingTaskRefsOptions type scripts/rederive-memory-associations.ts:84 — passes { skipQuotationElision: true } to extractTrackingTaskRefs for archaeology Adopted New exported options type added alongside an optional param on the existing function; existing consumers remain source-compatible. The sole intended consumer is the re-derivation script per in-code docs.

Documentation impact

  • no-update-needed — Adds an optional options bag to extractTrackingTaskRefs and introduces a standalone maintenance script plus tests. No user-facing CLI/HTTP surface or documented behavior changed. The new option defaults preserve existing behavior; no docs reference this internal extractor’s signature directly.

@minsky-ai minsky-ai Bot changed the title feat(mt#4765): Validate stored tracksTask associations against their text [no-deploy-impact] feat(mt#4765): Validate stored tracksTask associations against their text Aug 31, 2026
@edobry
edobry merged commit 329dbf5 into main Aug 31, 2026
20 checks passed
@edobry
edobry deleted the task/mt-4765 branch August 31, 2026 01:28

@minsky-reviewer minsky-reviewer Bot 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.

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2


Re-verification complete. The prior BLOCKING issues called out earlier (unguarded entrypoint, partial-corpus scan reporting, and apply-path rebuilding associations from a stale snapshot) are addressed in the current diff: scripts/rederive-memory-associations.ts is guarded by import.meta.main, pages through the entire corpus then asserts coverage against count(), and on --execute re-reads each record, skips on drift, and preserves other association keys while only updating tracksTask. The extractor change adds an optional skipQuotationElision flag with clear “archaeology-only” docs; default behavior for all existing callers is unchanged. The re-derivation script derives both ways using the shipped extractor, emits a dry-run plan with a stable token, and enforces token + drift checks on apply. Unit tests validate discrimination and token stability, and a measurement snapshot is committed for traceability. I find no new critical defects introduced by these changes. Verdict: APPROVE.

Spec verification

Criterion Status Evidence
SC1 — The write-time-minted population (16 records, created_at >= 2026-08-24T20:56:48Z, the derivation commit) is classified by hand: for each, the derived ref against the text that produced it, marked true/false. A rate, not an estimate, and stamped with its measurement time. Records in the commit-time ambiguity band (mem#1237) are classified and reported separately rather than assigned to a population. Met Execution artifact committed at scripts/rederive-memory-associations-results.json shows a measurement snapshot ("measuredAt": "2026-08-31T01:12:14.894Z") and includes mem#1237 in the plan (commit-time ambiguity band) plus per-record before/after classification entries. PR description enumerates the 16 write-time records and their verdicts; tests exercise the discrimination logic (scripts/rederive-memory-associations.test.ts:9-66).
SC2 — The same classification is run over a random sample of the 166 backfilled records large enough to bound their rate, and the sample size is stated with its basis. Met The script scans the entire corpus and tallies all refs carrying tracksTask (listEveryMemory with paging + count guard in scripts/rederive-memory-associations.ts:160-206). The committed results snapshot (scripts/rederive-memory-associations-results.json) reports a census (scanned: 1343, tally showing 179 grounded, 9 quoted-only, 10 not-derivable). The PR body states this is a census, not a sample, which satisfies and exceeds SC2.
SC3 — A re-derivation path exists: given a record, recompute tracksTask from current text under current patterns and report the diff. Dry-run by default per operational-safety-dry-run-first, --execute gated, and wrapped in this task (>10 records, so the bulk-mutation task wrapper applies). Met Implemented in scripts/rederive-memory-associations.ts. Core classification functions (classifyRecord, correctedRefs) use extractTrackingTaskRefs twice (current and raw) to re-derive and diff (lines ~64-116). CLI defaults to dry-run with summary/JSON output, and supports --execute gated by a plan token (lines ~208-392).
SC4 — Records whose association is confirmed false are corrected, with the before/after set recorded. tasks_bulk-edit-style dry-run-then-token discipline, or an equivalent recorded diff. Met Per the spec’s amendment, SC4 was split. SC4a (code) is satisfied: the script emits a plan with before/after and a stable token (planToken at scripts/rederive-memory-associations.ts:118-139) and --execute enforces token match, re-reads each record, aborts on drift, and preserves other association keys (lines ~264-352). The recorded diff is emitted to stdout and captured in scripts/rederive-memory-associations-results.json (plan array). SC4b (data run) is explicitly out-of-PR and remains gated by principal authorization (see spec amendment).
SC5 — The re-derivation is repeatable after mt#4454 lands, since that fix changes what the patterns produce; the mechanism is not a one-shot script that has to be rewritten then. Met packages/domain/src/memory/staleness.ts adds an optional options bag to extractTrackingTaskRefs supporting { skipQuotationElision } (lines 248-297 in diff), allowing both current (elided) and legacy-raw derivations via one exported function. The script calls both modes to classify and remains valid after mt#4454 (scripts/rederive-memory-associations.ts:92-113).
SC6 — A negative control: a record with a genuine, correctly-derived association is left untouched by the corrective pass. mem#1340 (false) and any confirmed-true record from SC1 make the discriminating pair. Met Unit tests assert discrimination: a genuine unquoted clause yields verdict "grounded" and is preserved while a quoted-only ref is dropped (scripts/rederive-memory-associations.test.ts:34-47, 20-33, 49-66). The committed plan shows multiple records where a true ref remains after correction (e.g., mem#315, mem#199, mem#82 keep mt#1034) in scripts/rederive-memory-associations-results.json.
SC4b — the pass is actually run against prod and the before/after set recorded in ## Findings (data operation, outside PR). N/A Per the spec’s 2026-08-30 amendment, SC4b is deliberately not fulfilled by any PR — it is a gated data operation requiring principal authorization. This PR supplies SC4a (code) only; no production data mutation is expected in the diff.

Adoption sweep

Symbol Kind Consumers found Classification Notes
ExtractTrackingTaskRefsOptions type Missing consumers New public type exported from packages/domain/src/memory/staleness.ts to carry the optional archaeology-only flag for extractTrackingTaskRefs. No direct imports of the type found; the only behavioral consumer is the re-derivation script which passes the options bag at call-site without importing the type. No wiring required by spec; non-blocking.

Recommendation: file a follow-up adoption task to wire 1 missing consumer.

Documentation impact

  • no-update-needed — Behavior for existing callers of extractTrackingTaskRefs is unchanged (the new options bag defaults to the same elided matching). The rest of the change adds an internal maintenance script and tests. No public CLI, API, or documented user-facing behavior changed. No docs in docs/ reference this archaeology-only option, and none need updating.

edobry added a commit that referenced this pull request Sep 1, 2026
… corpus

## Summary

mt#4761 gave `MemoryService.list()` a 500-record default cap (`DEFAULT_LIST_CAP`). Its own docblock
says the consequence out loud — *"`list()` deliberately stays capped, so its own array length cannot
answer 'how many rows actually match.'"* Four scripts asked it exactly that question and treated the
answer as the whole corpus.

They fail silently, which is the point: a capped read returns a plausible array, every figure derived
from it is internally consistent, and the process exits 0. Reproduced against prod before touching

- `backfill-memory-associations.ts` → `Found 500 memories to scan.`
- `normalize-memory-associations.ts` → `Scanned 500 memories.` / `divergent keys: 0`

The second is a clean bill of health for a corpus it never looked at 62% of.

## Key changes

**`scripts/lib/list-every-memory.ts` (new).** Pages the corpus, then asserts coverage against
`count()` and throws on a short read — so the scan is able to FAIL (mem#704: a probe that returns
the same result when the system is broken is not verification). Promoted out of
`rederive-memory-associations.ts`, which held the only correct copy.

It lives in `scripts/lib/`, not `packages/domain/`, deliberately: it materializes the entire corpus,
which is precisely what mt#4761 removed from the domain read surface. Correct for an offline script
deciding a write set, wrong for a request path — the module boundary keeps that legible.

**Four census callers now consume it**, one of which the spec did not name:

- `backfill-memory-associations.ts:87` — was `list({})`.
- `normalize-memory-associations.ts:214` — was `list({})`.
- `rederive-memory-associations.ts:260` — already correct; now consumes the shared helper.
- **`import-claude-code-memory.ts:290`** — was a bare `list()`, and this one is worse than the
  others. That read builds the importer's **idempotency hash set**, then it calls `create()`. A
  capped scan does not merely under-report: every memory outside the 500-row window is invisible to
  the duplicate check and gets re-imported. Found by SC2's own audit, not by the spec.

**`backfill`'s entrypoint is guarded with `import.meta.main` (SC5).** Unguarded, a bare import
initialized config, opened Postgres, began a full scan, and could `process.exit` out from under its
caller. The reviewer raised this as BLOCKING against the sibling script in PR #3496.

**One inherited comment corrected rather than copied forward.** The private implementation claimed
that comparing with `<` absorbs a concurrent DELETE. It does not — a delete lowers the scan below a
floor already taken, so it throws. That is the right trade (fail closed; offset paging can genuinely
skip rows during deletion), and the comment now says so instead of asserting the opposite.

## Deviation from the spec, recorded

SC1 named `backfill` alone; SC2 asked for the others to be "fixed the same way or recorded as
intentionally paginated." This extracts ONE shared helper and routes all four through it, per the
SC2 amendment made during planning. The mechanism it rests on: four copies of a coverage contract is
the divergent-copy shape ADR-024 centralised a pass to prevent on another surface (mt#4793), and the
copies had already begun to drift — see the corrected comment above.

## Scope check

`isDeploySurfaceFile` returns **false** for all 7 changed files, run over the actual diff rather
than recalled — hence `[no-deploy-impact]`. No new external-system integration; no contract change
(`count` was already on `MemoryServiceSurface`).

## Testing

Execution evidence:

**SC3 / AT3** — 6 new tests, `scripts/lib/list-every-memory.test.ts`:

```
$ bun test --preload ./tests/setup.ts --timeout=15000 scripts/lib/list-every-memory.test.ts
 6 pass
 0 fail
 9 expect() calls
Ran 6 tests across 1 file. [57.00ms]
```

**AT1** — fixed backfill, dry-run against live prod. Asserted against the live `count()`, not a
literal; the corpus grows continuously (1,347 six minutes earlier):

```
$ bun scripts/backfill-memory-associations.ts
Found 1349 memories to scan.
DRY RUN — no changes written. Pass --execute to apply.
EXIT=0
```

**AT5** — fixed normalize, dry-run against live prod. `divergent keys: 0` is now a corpus-wide
result rather than one over 37% of it, which incidentally confirms mt#4448's normalization held
across the whole corpus:

```
$ bun scripts/normalize-memory-associations.ts
Scanned 1349 memories.
Records with divergent keys:      0
Divergent values total:           0
```

**AT4** — entrypoint guard, both directions, as PR #3496 did:

```
=== bare import of the GUARDED module ===
IMPORT_COMPLETED_INERT

=== same module, guard condition forced true (pre-fix behaviour) ===
IMPORT_COMPLETED
info: Persistence provider created: PostgresVectorPersistenceProvider
info: PersistenceService initialized successfully
Found 1349 memories to scan.
```

**AT2 / SC2** — call-site audit table with a verdict per site, in the spec's `## Outcome`. Four
FIXED; four verified intentionally paginated (`memory-bundle.ts` and `memories-stats.ts` carry
explicit limits, `memories-list.ts` and the `memory.list` command are mt#4761's own paginated
consumers).

**SC1 / SC5** — covered by AT1 and AT4 respectively, above.

Negative control — SC3 fail-closed assertion: removing `if (out.length < floor)` fails exactly the two coverage tests, and nothing else.

```
guard neutralized
(fail) listEveryMemory > throws rather than returning a partial scan when paging stops short
(fail) listEveryMemory > refuses when the service silently caps every page below the requested limit
 4 pass
 2 fail
```

Negative control — SC3 missing-count refusal: removing it fails exactly the one no-count test.

```
refusal neutralized
(fail) listEveryMemory > throws when the service exposes no count(), rather than trusting list()
 5 pass
 1 fail
```

Restoring the file returns 6 pass / 0 fail.

The first negative control I ran was invalid and is worth naming: I copied the module to `/tmp` and
imported it, which failed on module resolution (`./lib/list-every-memory` does not exist there), and
the empty output would have read as "the guard works." Re-run inside `scripts/` with stderr visible,
it produced the evidence above.

**Related tests** (the fast gate's own selection):

```
$ bun scripts/run-related-tests.ts scripts/lib/list-every-memory.ts scripts/backfill-memory-associations.ts \
    scripts/normalize-memory-associations.ts scripts/rederive-memory-associations.ts scripts/import-claude-code-memory.ts
 61 pass
 0 fail
run-related-tests.ts: 4 related test file(s) passed
```

**Typecheck** — clean across all 8 projects (`.`, `packages/domain`, `packages/shared`,
`services/reviewer`, `services/site`, `src/cockpit/web`, `tsconfig.hooks.json`,
`tsconfig.scripts.json`); `infra/tsconfig.json` skipped with its documented reason.
**Lint** — 0 errors, 0 warnings across 4,294 files. **Format** — `format:check` exit 0.

## Test-double change

`tests/scripts/import-claude-code-memory.test.ts`'s fake now honours `limit`/`offset` and implements
`count()`. `count` is OPTIONAL on `MemoryServiceSurface` and the fake omitted it, so the census
refused — correctly, and 18 tests went red until the double implemented the surface it claims to.
The fake also returned its whole array for every page regardless of the filter, which would have
spun forever once a fixture reached the page size.

Co-Authored-By: minsky-ai[bot] <minsky-ai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authorship/co-authored Co-authored by human and AI agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant