Conversation
…tracking (swamp-club#232) Adds an optional `relPath` field to `DatastoreSyncOptions` so swamp core can attribute each `markDirty` call to a specific cache-relative path. Strictly additive: existing extensions, call sites, and bulk-invalidate semantics are unchanged. Per-key-queryable backends (Mongo/Redis/etcd-style) can now record exactly which paths changed and skip the full-cache walk on `pushChanged`. The contract is documented in eight load-bearing rules on the JSDoc and in `design/datastores.md` "markDirty() contract" section: pre-write timing, absence-on-disk = delete, undefined = bulk, restart-loses-set, cache-relative + forward-slash with native-separator conversion on Windows, backward compatibility, field scope, and bulk-overrides-per-path-within-one-operation. Internal `MarkDirtyHook` becomes positional `(relPath?: string)` because repositories don't have an `AbortSignal` in scope; the wiring layer in `repo_context.ts` packs it into the public-interface options bag and owns the absolute-to-cache-relative + forward-slash normalization. Per-call granularity emitted by core: - save / append / allocateVersion: data-name directory (version not yet allocated) - removeLatestMarker: data-name directory - delete (with version): version directory - delete (no version): data-name directory (whole subtree) - finalizeVersion: version directory - yaml save / delete: per-yaml file path - rename / collectGarbage / deleteAllByWorkflowId / clearAll: undefined (bulk) End-to-end verified against an out-of-tree Mongo-backed datastore extension: relPath flows correctly, per-path pushChanged works with subtree expansion, tombstone safety gate holds. Closes swamp-club#232
There was a problem hiding this comment.
Code Review
Clean, well-structured PR that threads relPath through the markDirty contract for per-path dirty tracking. Backward compatible, thoroughly tested, cross-platform safe.
Blocking Issues
None.
Suggestions
notifyDirtyJSDoc vs parameter name (unified_data_repository.ts:460): The JSDoc says "relPath is the absolute path of the file or directory" but the parameter name isrelPath, which could mislead readers into thinking it's already cache-relative. The wiring layer comment explains the conversion, but renaming the parameter toabsPath(matchingbuildMarkDirtyHook's parameter name) would eliminate ambiguity. Minor — theMarkDirtyHooktype signature makes the intent clear.
What looks good
- DDD alignment:
buildMarkDirtyHooklives in the composition root (repo_context.ts) where wiring belongs. Repositories don't know about cache roots — proper separation of concerns. - Contract design: The 8-rule contract on
markDirtyis thorough and well-placed — in the JSDoc, design doc, and extension docs simultaneously. - Backward compatibility:
relPathis optional throughout; existing implementations are unaffected. - Test coverage: New
yaml_evaluated_definition_repository_test.ts, updated tests for all four repository types, end-to-end wiring test inrepo_context_test.ts, and type-compat test updated. - Cross-platform: Forward-slash normalization via
SEPARATORcheck inbuildMarkDirtyHook, with the wiring test exercising paths containing separators to catch Windows regressions. yaml_output_repository.deleteimprovement: MovingnotifyDirtyinside the loop so it only fires when a match is found (and passing the resolved path) is a nice refinement — no spurious dirty signals on no-op deletes, while maintaining the pre-write timing contract.- Security:
relative()output is metadata/hint only; actual file I/O continues throughassertSafePath.
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity findings.
Medium
removeLatestMarkerhas no per-pathrelPathtest —src/infrastructure/persistence/unified_data_repository_test.ts: The test exercisessave,append,allocateVersion,finalizeVersion,rename,delete(both forms), andcollectGarbage, butremoveLatestMarkeris missing. It passesdataNameDirtonotifyDirty, which is structurally identical to the other per-path cases, but the lack of test coverage means a future refactor could regress the path argument without detection. Not blocking because the implementation is trivially correct, but worth adding.
Low
-
buildMarkDirtyHookdoes not validate thatabsPathis undercacheRoot—src/cli/repo_context.ts:146: If a caller passes an absolute path outside the cache root,relative(cacheRoot, absPath)produces a../-prefixed string. In practice this cannot happen because all repositorybaseDirvalues are resolved undercacheRootviadsPath()in the repository factory, and allgetPath/getDataNameDirmethods join onbaseDir. The invariant is sound but implicit — if a future change wires a repo whosebaseDiris not undercacheRoot, therelPathwould contain..components and confuse extensions. Documenting this precondition in a one-line assertion or comment would be cheap insurance. -
Inline type
{ relPath?: string }in test mock vsDatastoreSyncOptions—packages/testing/datastore_test_context.ts:215: ThemarkDirtymock parameter is typed as{ relPath?: string }rather thanDatastoreSyncOptions. Structurally compatible in TypeScript, but ifDatastoreSyncOptionsgains a new field that extensions rely on, this mock won't exercise it. Cosmetic — not blocking. -
Example
pushChangeddoes a full walk whendirty.size === 0 && !bulkInvalidated—.claude/skills/swamp-extension-datastore/references/examples.md:128-132: This is correct for the "first push after process restart" case (rule 4), but means every idle push does a full walk even when nothing changed. The comment mentions rule 4 but the code could be clearer — e.g. a separatefirstPushflag. This is example/documentation code, not production, so purely advisory.
Verdict
PASS — Strictly additive, well-documented contract, thorough test coverage on the main code paths, backward-compatible interface evolution. The change is sound and ready to merge.
Summary
Adds an optional
relPathfield toDatastoreSyncOptionsso swamp core canattribute each
markDirtycall to a specific cache-relative path. Strictlyadditive: existing extensions, call sites, and bulk-invalidate semantics are
unchanged. Per-key-queryable backends (Mongo/Redis/etcd-style) can now record
exactly which paths changed and skip the full-cache walk on
pushChanged.The eight-rule contract is on the
markDirtyJSDoc and indesign/datastores.md"markDirty() contract": pre-write timing,absence-on-disk = delete, undefined = bulk, restart-loses-set, cache-relative
compatibility, field scope, bulk-overrides-per-path-within-one-operation.
Per-call granularity emitted by core
relPathsave,append,allocateVersionremoveLatestMarkerdelete(version)delete()finalizeVersionsave,deleterename,collectGarbage, bulk yaml deletesundefined(bulk)The internal
MarkDirtyHookis positional(relPath?: string)becauserepositories don't have an
AbortSignalin scope; the wiring layer inrepo_context.ts(buildMarkDirtyHookhelper) packs into the public optionsbag and owns the absolute → cache-relative + forward-slash normalization.
Test plan
deno fmt --checkclean (1311 files)deno lintclean (1185 files)deno checkcleandeno run test— 5328 passed, 0 faileddeno run compile— binary builtrelPathper granularity (incl. newyaml_evaluated_definition_repository_test.ts).repo_context_test.tsend-to-end wiring assertion with Windows-aware forward-slash check.testing_package_compat_test.tsexercises bothmarkDirty()andmarkDirty({ relPath }).renameordering test pins rule 8: bulk signal emitted before the innersave()'s per-path signal.Closes swamp-club#232