docs(6205): snippets comments state the reason, not the instance that taught it
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 22s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m8s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 22s
Issue, decision, milestone and rule citations are removed; the snippet-mirror docstring drops the paragraph about its own earlier wording, and the pointer-model and freshness comments state the design rather than the plan step that introduced it. Access notes point at services/access.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ has (embeddings, ACL, project/System association, dedup) and, crucially, becomes
|
||||
eligible for semantic recall the moment it's embedded:
|
||||
|
||||
- ``title`` = ``name``. The trigger joins it only in the embedded document
|
||||
(``embeddings.document_title``, milestone 427).
|
||||
(``embeddings.document_title``).
|
||||
- ``tags`` = ``[language, "snippet", *caller_tags]``.
|
||||
- ``body`` = templated markdown (When to use / Signature / Location, then a
|
||||
fenced code block, then an optional ``## Notes`` section).
|
||||
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
SNIPPET_NOTE_TYPE = "snippet"
|
||||
SNIPPET_TAG = "snippet"
|
||||
# The body section free-text notes live under (#4378). A heading rather than a
|
||||
# The body section free-text notes live under. A heading rather than a
|
||||
# `**Notes:**` line because notes are paragraphs, and a heading is the boundary
|
||||
# `embeddings.chunk_document` splits at.
|
||||
NOTES_HEADING = "## Notes"
|
||||
@@ -103,11 +103,11 @@ def resolve_locations(
|
||||
are the single-location shorthand and apply only when `locations` was not
|
||||
given — passing both is not a merge, it is the caller having decided.
|
||||
|
||||
Extracted because compose_body, create_snippet and the dedup gate must all
|
||||
read the shorthand the SAME way. They each had their own copy of the
|
||||
`if locations is None` fallback, which is fine until one of them gains a
|
||||
rule the others don't — and the gate (#2518) is the one where a disagreement
|
||||
would mean comparing a location the record won't actually be stored with.
|
||||
One function because compose_body, create_snippet and the dedup gate must
|
||||
all read the shorthand the SAME way. Three copies of the
|
||||
`if locations is None` fallback are fine until one of them gains a rule
|
||||
the others don't — and the gate is the one where a disagreement would mean
|
||||
comparing a location the record won't actually be stored with.
|
||||
"""
|
||||
if locations is None:
|
||||
locations = [{"repo": repo, "path": path, "symbol": symbol}]
|
||||
@@ -138,8 +138,8 @@ def _normalize_merged_from(entries: list | None) -> list[dict]:
|
||||
Order is history, not sorting — earlier merges stay first, so the list reads
|
||||
as the sequence of things folded in.
|
||||
|
||||
Each entry records WHAT THAT SOURCE CONTRIBUTED, which is what makes un-merge
|
||||
(#2165) exact. Two problems it solves at once:
|
||||
Each entry records WHAT THAT SOURCE CONTRIBUTED, which is what makes
|
||||
un-merge exact. Two problems it solves at once:
|
||||
|
||||
- A location can arrive from a source AND genuinely be the survivor's own.
|
||||
Recording only what the source ADDED means reversing it can never strip a
|
||||
@@ -200,7 +200,7 @@ def compose_body(
|
||||
"""Render structured fields into the snippet body markdown. Empty fields are
|
||||
omitted so the body stays clean.
|
||||
|
||||
``notes`` is the free text a snippet had no home for (#4378): why it is
|
||||
``notes`` is the free text a snippet has no other home for: why it is
|
||||
shaped this way, what superseded what, the caveats. Without it that prose
|
||||
went into ``when_to_use`` — the trigger, which is joined onto the title of
|
||||
EVERY chunk the snippet is embedded as, so a 3 KB write-up there blurred
|
||||
@@ -371,7 +371,7 @@ _DATA_FIELDS = (
|
||||
"verification", "provenance",
|
||||
)
|
||||
|
||||
# --- drift check (#2086) -----------------------------------------------------
|
||||
# --- drift check -------------------------------------------------------------
|
||||
# A recorded snippet points at a repo · path · symbol that WILL rot: files move,
|
||||
# symbols get renamed, implementations diverge from the copy stored here. Left
|
||||
# undetected, the record degrades from "canonical reference" to "confidently
|
||||
@@ -379,9 +379,9 @@ _DATA_FIELDS = (
|
||||
# same authority either way.
|
||||
#
|
||||
# WHERE THE CHECK RUNS. Not here. Scribe has no checkout of the operator's repos
|
||||
# and must not acquire one (rule #115 — the instance stays agnostic about where
|
||||
# code lives; giving the server repo access would make every install a
|
||||
# credential problem). The agent already has the working tree, so IT does the
|
||||
# and must not acquire one (the instance stays agnostic about where code
|
||||
# lives; giving the server repo access would make every install a credential
|
||||
# problem). The agent already has the working tree, so IT does the
|
||||
# comparing and reports a verdict; the server's job is to remember the verdict,
|
||||
# make it queryable, and know when it has expired.
|
||||
#
|
||||
@@ -445,10 +445,10 @@ def compose_verification(
|
||||
out["detail"] = detail.strip()
|
||||
if (path or "").strip():
|
||||
out["path"] = path.strip()
|
||||
# The repo commit the working tree was at when the check ran (#2688). The
|
||||
# The repo commit the working tree was at when the check ran. The
|
||||
# code_sha above expires a verdict when the RECORD is edited; this makes
|
||||
# "the REPO moved on since the check" computable too, once the forge
|
||||
# integration can compare it against the current head.
|
||||
# "the REPO moved on since the check" computable too, against the forge's
|
||||
# current head (`_verdict_still_vouches`).
|
||||
if (commit_sha or "").strip():
|
||||
out["commit_sha"] = commit_sha.strip()
|
||||
return out
|
||||
@@ -458,12 +458,11 @@ def compose_provenance(*, commit_sha: str, fetched_at: str = "") -> dict | None:
|
||||
"""Build the `data.provenance` record: which commit the cached body was
|
||||
read at, and when.
|
||||
|
||||
This is the pointer-model half of decision #2686 — the recorded location
|
||||
is the source of truth for the code and the stored body is a CACHE of it.
|
||||
Provenance says what that cache is a cache OF, so a reader (and later the
|
||||
forge fetch, step 5 of milestone 288) can judge staleness instead of
|
||||
guessing. Absent provenance is valid and means exactly what every snippet
|
||||
meant before this existed: a body captured by hand at an unknown point.
|
||||
The pointer model: the recorded location is the source of truth for the
|
||||
code and the stored body is a CACHE of it. Provenance says what that cache
|
||||
is a cache OF, so a reader (and the forge fetch at pull time) can judge
|
||||
staleness instead of guessing. Absent provenance is valid and means a body
|
||||
captured by hand at an unknown point.
|
||||
"""
|
||||
sha = (commit_sha or "").strip()
|
||||
if not sha:
|
||||
@@ -493,7 +492,7 @@ def verification_view(note, fields: dict) -> dict:
|
||||
"detail": stored.get("detail"),
|
||||
"path": stored.get("path"),
|
||||
"commit_sha": stored.get("commit_sha"),
|
||||
# A push touched the recorded location since this verdict (#2691) —
|
||||
# A push touched the recorded location since this verdict —
|
||||
# the repo moved under it. Cleared by the next verdict, which builds
|
||||
# a fresh dict.
|
||||
"invalidated_by": stored.get("invalidated_by"),
|
||||
@@ -548,7 +547,7 @@ def compose_data(
|
||||
# either: the verdict's code_sha expires it on read if the code moved on.
|
||||
if verification:
|
||||
out["verification"] = verification
|
||||
# Also carried: what commit the cached body was read at (#2688). The caller
|
||||
# Also carried: what commit the cached body was read at. The caller
|
||||
# owns the live-or-die rule — update_snippet drops it when the code changes
|
||||
# without a fresh SHA, because keeping it would claim the new body came
|
||||
# from the old commit.
|
||||
@@ -565,7 +564,7 @@ def compose_data(
|
||||
return out
|
||||
|
||||
|
||||
# Past this, a trigger is advised to move its explanation into `notes` (#4378).
|
||||
# Past this, a trigger is advised to move its explanation into `notes`.
|
||||
# The trigger is joined onto the title of EVERY chunk (`chunk_document`), so at
|
||||
# 600 characters it is already over 40% of a 1,400-character chunk — each
|
||||
# vector is then more about the trigger's prose than about the code or the
|
||||
@@ -605,14 +604,13 @@ def recompose_data(note) -> dict:
|
||||
For the GENERIC note door. `update_snippet` composes the mirror itself from
|
||||
the field set it just merged and never needs this; a plain
|
||||
`update_note(body=...)` — which the Knowledge feed's editor issues, because
|
||||
a snippet card there routes to /notes/:id — has no idea the mirror exists,
|
||||
and left it stale. `snippet_fields` then PREFERS the stale mirror, so the
|
||||
row reported its old repo/path/symbol to prior-art recall while displaying
|
||||
its new body: confidently wrong, which is worse than no record (#3128).
|
||||
a snippet card there routes to /notes/:id — has no idea the mirror exists.
|
||||
Left stale, the mirror wins: `snippet_fields` PREFERS it, so the row would
|
||||
report its old repo/path/symbol to prior-art recall while displaying its
|
||||
new body — confidently wrong, which is worse than no record.
|
||||
|
||||
The body is the authority; the mirror is derived. That is already the rule
|
||||
this file states — it just had no enforcement on the path that bypasses
|
||||
`update_snippet`.
|
||||
The body is the authority; the mirror is derived. This is that rule's
|
||||
enforcement on the path that bypasses `update_snippet`.
|
||||
|
||||
`verification` and `provenance` are CARRIED, not recomposed, because
|
||||
neither is in the body to parse — the same carry `compose_data` does for
|
||||
@@ -642,15 +640,11 @@ def snippet_fields(note) -> dict:
|
||||
THE BODY IS THE AUTHORITY; `data` is a mirror derived from it. Every writer
|
||||
keeps them in step — the snippet service composes the mirror from the field
|
||||
set it just merged, `update_note` recomposes it when a body reaches the
|
||||
generic door (#3128), and `backfill_snippet_data` filled the pre-0070 rows
|
||||
generic door, and `backfill_snippet_data` fills any row without a mirror
|
||||
at startup. The fallback below is therefore a belt to that braces, not a
|
||||
second source of truth: it is what a row looks like before the backfill has
|
||||
run, and it must keep agreeing with the mirror.
|
||||
|
||||
(This docstring used to say those rows were "never backfilled". That was
|
||||
true when 0070 landed and stopped being true when the backfill shipped; it
|
||||
is corrected here because the sentence read as licence for a stale mirror,
|
||||
which is exactly the bug #3128 found.)
|
||||
run, and it must keep agreeing with the mirror. A stale mirror is a bug,
|
||||
never a state to tolerate.
|
||||
|
||||
`code` only ever comes from the body, since `data` doesn't carry it.
|
||||
"""
|
||||
@@ -678,7 +672,7 @@ async def backfill_snippet_data(*, batch: int = 500) -> int:
|
||||
|
||||
Migration 0070 deliberately left `data` NULL on existing rows, because
|
||||
*reading* a snippet never needed it — `snippet_fields` falls back to parsing
|
||||
the body. Querying does: the location reverse lookup (#2083) is a jsonpath
|
||||
the body. Querying does: the location reverse lookup is a jsonpath
|
||||
over this column, so a NULL-`data` snippet would be reported as "nothing
|
||||
recorded here" and the caller would write the helper again — silently worse
|
||||
than having no reverse lookup at all. Every consumer of the column
|
||||
@@ -779,8 +773,8 @@ async def create_snippet(
|
||||
``repo``/``path``/``symbol`` are the one-location shorthand.
|
||||
|
||||
``commit_sha`` stamps the body's provenance — the commit the recording
|
||||
session read the code at (#2688). Optional: absent means what it always
|
||||
meant, a body captured at an unknown point."""
|
||||
session read the code at. Optional: absent means a body captured at an
|
||||
unknown point."""
|
||||
locations = resolve_locations(repo, path, symbol, locations)
|
||||
note = await notes_svc.create_note(
|
||||
user_id,
|
||||
@@ -807,9 +801,10 @@ async def get_snippet(user_id: int, snippet_id: int):
|
||||
"""Fetch a snippet by id, or None if it doesn't exist / isn't a snippet /
|
||||
isn't readable by this user.
|
||||
|
||||
Share-aware (rule #78): a fetch by id is an explicit act, so it resolves the
|
||||
caller's full read scope rather than ownership alone. Without this, a snippet
|
||||
that a search legitimately surfaced could not then be opened — see #2093."""
|
||||
Share-aware (services/access.py): a fetch by id is an explicit act, so it
|
||||
resolves the caller's full read scope rather than ownership alone. Without
|
||||
this, a snippet that a search legitimately surfaced could not then be
|
||||
opened."""
|
||||
result = await notes_svc.get_note_for_user(user_id, snippet_id)
|
||||
if result is None:
|
||||
return None
|
||||
@@ -883,12 +878,12 @@ async def update_snippet(
|
||||
merged field set back into title/body/tags. Returns the Note, or None if the
|
||||
id isn't a snippet the caller can see.
|
||||
|
||||
``commit_sha`` restamps the body's provenance (#2688). It lives or dies
|
||||
``commit_sha`` restamps the body's provenance. It lives or dies
|
||||
with the code: passed → restamped at that commit; code changed without it →
|
||||
dropped, because keeping it would claim the new body came from the old
|
||||
commit; code untouched → carried.
|
||||
|
||||
Share-aware (rule #47/#78): resolves the read scope, then requires WRITE —
|
||||
Share-aware (services/access.py): resolves the read scope, then requires WRITE —
|
||||
so an editor/admin grant lets the holder edit, and a viewer grant does not.
|
||||
Raises PermissionError when the caller can read but not write, because "not
|
||||
found" would be a lie about a record they can plainly open. The write itself
|
||||
@@ -930,7 +925,7 @@ async def update_snippet(
|
||||
else:
|
||||
merged_locations = cur["locations"]
|
||||
|
||||
# Provenance follows the code (#2688): a fresh SHA restamps it; a code
|
||||
# Provenance follows the code: a fresh SHA restamps it; a code
|
||||
# change without one drops it; an edit that leaves the code alone carries
|
||||
# it. Order matters — the explicit SHA wins even when the code changed,
|
||||
# because that is precisely the caller saying where the new body came from.
|
||||
@@ -1037,7 +1032,7 @@ async def record_verification(
|
||||
# An "ok" verdict at a known commit IS a provenance claim — the checker
|
||||
# just established that the cached body matches the source there — so
|
||||
# it restamps. Any other verdict carries what was known: a verdict is
|
||||
# about the code, not a change to it, and must not erase it (#2688).
|
||||
# about the code, not a change to it, and must not erase it.
|
||||
provenance=(
|
||||
compose_provenance(commit_sha=commit_sha)
|
||||
if status == VERIFY_OK and (commit_sha or "").strip()
|
||||
@@ -1047,7 +1042,7 @@ async def record_verification(
|
||||
return await notes_svc.update_note(note.user_id, snippet_id, data=data)
|
||||
|
||||
|
||||
# --- pull-time freshness (#2690) ---------------------------------------------
|
||||
# --- pull-time freshness -----------------------------------------------------
|
||||
# A pull is the moment freshness matters: the reader is about to trust the
|
||||
# cached body. When the record owner's keyring serves a forge, the pull
|
||||
# fetches the recorded file and answers the one mechanically-answerable
|
||||
@@ -1057,12 +1052,11 @@ async def record_verification(
|
||||
# fetching can honestly deliver, and divergence is reported, not overwritten.
|
||||
#
|
||||
# With no forge configured this function attaches NOTHING — the response is
|
||||
# byte-identical to pre-forge behavior (rule #115's baseline).
|
||||
# exactly what an install without a forge gets everywhere else.
|
||||
|
||||
# Total budget for the in-pull fetch. Tighter than the adapter's own timeout:
|
||||
# the pull is the moment a session decides whether pulling is worth it
|
||||
# (#2663's pull-through finding), so a slow forge must cost bounded time and
|
||||
# then the cache serves.
|
||||
# the pull is the moment a session decides whether pulling is worth it, so a
|
||||
# slow forge must cost bounded time and then the cache serves.
|
||||
PULL_FETCH_BUDGET_S = 2.5
|
||||
|
||||
|
||||
@@ -1099,8 +1093,8 @@ def _verdict_still_vouches(note, fields: dict, fetched_commit_sha: str) -> bool:
|
||||
"""Does a standing `ok` verdict still speak for this body, at this commit?
|
||||
|
||||
Containment (cached code ∈ fetched file) is the fast path, and it is right
|
||||
for a record kept verbatim. It is WRONG for a deliberately annotated one
|
||||
(#2782): a record whose job is to say why the shape is what it is carries
|
||||
for a record kept verbatim. It is WRONG for a deliberately annotated one:
|
||||
a record whose job is to say why the shape is what it is carries
|
||||
commentary the source does not, so containment fails forever and the record
|
||||
reads `diverged` on every pull. That turns the one honest drift signal into
|
||||
a permanent false positive — and annotation is a sanctioned record style,
|
||||
@@ -1109,15 +1103,14 @@ def _verdict_still_vouches(note, fields: dict, fetched_commit_sha: str) -> bool:
|
||||
The escape hatch is the verdict itself. `verify_snippet` is precisely where
|
||||
a human or agent already judged this body a faithful rendering of that
|
||||
source, and `verification.commit_sha` records the repo commit they judged
|
||||
it at — a field whose own docstring (#2688) anticipated this use: "makes
|
||||
'the REPO moved on since the check' computable, once the forge integration
|
||||
can compare it against the current head." This is that comparison.
|
||||
it at, which makes "the REPO moved on since the check" computable. This
|
||||
is that comparison.
|
||||
|
||||
All four conditions, and none is optional:
|
||||
- the verdict says `ok`;
|
||||
- it has not EXPIRED — `verification_view` recomputes `code_sha` against
|
||||
the record's current body, so editing the record retires the verdict;
|
||||
- it was not INVALIDATED by a push touching the location (#2691);
|
||||
- it was not INVALIDATED by a push touching the location;
|
||||
- the file we just fetched is at the very commit the verdict was stamped
|
||||
at. Any later commit means nobody has judged what is there now.
|
||||
|
||||
@@ -1136,10 +1129,9 @@ def _verdict_still_vouches(note, fields: dict, fetched_commit_sha: str) -> bool:
|
||||
|
||||
|
||||
async def attach_live_body(note, data: dict) -> None:
|
||||
"""Decorate a PULL response with forge-checked freshness (#2690).
|
||||
"""Decorate a PULL response with forge-checked freshness.
|
||||
|
||||
Adds, when (and only when) the record owner's keyring serves a forge
|
||||
(#2778):
|
||||
Adds, when (and only when) the record owner's keyring serves a forge:
|
||||
- ``body_source``: "forge" (confirmed against the source just now) or
|
||||
"cache" (the stored body, for whatever reason follows)
|
||||
- ``body_freshness``: "current" | "diverged" | "missing" |
|
||||
@@ -1156,7 +1148,7 @@ async def attach_live_body(note, data: dict) -> None:
|
||||
from scribe.services.forge import ForgeError, ForgeNotFound, get_forges
|
||||
|
||||
try:
|
||||
# The OWNER's keyring, honoring the project pin (#2778) — freshness
|
||||
# The OWNER's keyring, honoring the project pin — freshness
|
||||
# for a record is checked with its owner's credential, never the
|
||||
# reader's.
|
||||
selector = await get_forges(note.user_id, getattr(note, "project_id", None))
|
||||
@@ -1183,7 +1175,7 @@ async def attach_live_body(note, data: dict) -> None:
|
||||
return
|
||||
# Recorded location repos are free-form names ("Scribe"), which can't
|
||||
# address a forge API — the project's repo BINDING is the identity that
|
||||
# can (#2691). Try the location string first (it may be a real remote),
|
||||
# can. Try the location string first (it may be a real remote),
|
||||
# then fall back to the bindings of the snippet's project.
|
||||
resolved = selector.resolve(loc["repo"])
|
||||
if resolved is None and getattr(note, "project_id", None):
|
||||
@@ -1202,7 +1194,7 @@ async def attach_live_body(note, data: dict) -> None:
|
||||
stored_prov_sha = (fields.get("provenance") or {}).get("commit_sha") or ""
|
||||
|
||||
async def _probe():
|
||||
# Cached-SHA short-circuit (#2693): provenance names the commit the
|
||||
# Cached-SHA short-circuit: provenance names the commit the
|
||||
# cached code was last confirmed at, so one cheap "newest commit
|
||||
# touching this path" call can prove the file hasn't moved since —
|
||||
# no content transfer. That economy is what fits pull-time freshness
|
||||
@@ -1266,7 +1258,7 @@ async def attach_live_body(note, data: dict) -> None:
|
||||
elif _verdict_still_vouches(note, fields, fetched.commit_sha or ""):
|
||||
# Containment failed, but an unexpired `ok` verdict stamped at exactly
|
||||
# this commit already judged this body a faithful rendering of it —
|
||||
# the annotated-record case (#2782). Trust the judgment over the
|
||||
# the annotated-record case. Trust the judgment over the
|
||||
# substring test; `data["verification"]` travels in the same payload,
|
||||
# so a reader can see the basis rather than take "current" on faith.
|
||||
data["body_source"] = "forge"
|
||||
@@ -1276,7 +1268,7 @@ async def attach_live_body(note, data: dict) -> None:
|
||||
data["body_freshness"] = "diverged"
|
||||
|
||||
|
||||
# --- push-time drift flagging (#2691) ----------------------------------------
|
||||
# --- push-time drift flagging ------------------------------------------------
|
||||
|
||||
def _path_touches(recorded: str, changed: str) -> bool:
|
||||
"""The location-path semantics, applied to a pushed file: the recorded path
|
||||
@@ -1294,7 +1286,7 @@ async def invalidate_for_push(
|
||||
removed: list[str],
|
||||
commit_sha: str,
|
||||
) -> int:
|
||||
"""Flag snippets whose recorded location a push just touched (#2691).
|
||||
"""Flag snippets whose recorded location a push just touched.
|
||||
|
||||
Writes ``verification.invalidated_by = {commit_sha, at, path, removed}``
|
||||
onto matched snippets that CARRY a verdict — the flag means "the repo
|
||||
@@ -1449,7 +1441,7 @@ async def merge_snippets(user_id: int, target_id: int, source_ids: list[int]):
|
||||
Share-aware and write-gated, like update: an editor/admin grant is enough, a
|
||||
viewer grant is not. Raises PermissionError when the caller can read the
|
||||
target but not write it. Every source must share the TARGET'S OWNER —
|
||||
cross-owner merge stays out of scope (#231) — and must itself be writable;
|
||||
cross-owner merge is out of scope — and must itself be writable;
|
||||
sources failing either test are skipped rather than silently half-merged.
|
||||
|
||||
The survivor records what it absorbed as `merged_from` — in the `data` mirror
|
||||
@@ -1499,7 +1491,7 @@ async def merge_snippets(user_id: int, target_id: int, source_ids: list[int]):
|
||||
# a target merged twice keeps both histories.
|
||||
#
|
||||
# Each entry also carries what THAT source contributed, which is what makes
|
||||
# un-merge exact (#2165) rather than a blind subtraction that would strip
|
||||
# un-merge exact rather than a blind subtraction that would strip
|
||||
# call sites the survivor legitimately owns.
|
||||
merged_from = _normalize_merged_from(
|
||||
list(tgt_fields.get("merged_from") or [])
|
||||
@@ -1561,8 +1553,8 @@ async def unmerge_snippet(user_id: int, survivor_id: int, source_id: int):
|
||||
one record type. Instead un-merge performs the restore itself, so the inverse
|
||||
is one operation with one authorization check and the trash path stays
|
||||
ignorant. Restoring from the trash directly is still allowed and still leaves
|
||||
both records claiming the same call sites — which is why this exists — but it
|
||||
is no longer the only way back.
|
||||
both records claiming the same call sites — which is why this exists — so it
|
||||
is not the only way back.
|
||||
|
||||
EXACTNESS. Subtraction uses the contribution recorded at merge time, not the
|
||||
source's current locations. A source that was itself edited after being
|
||||
|
||||
Reference in New Issue
Block a user