From f11014e4bcd63a5b61f12e21798ef32c152c621a Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Mon, 27 Jul 2026 02:14:01 -0700 Subject: [PATCH] Follow Tangled comments across account boundaries. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read feed-comment strong references from each author PDS so the decision queue receives the answer instead of a false empty thread. Defense: wiki/process/tick.md makes answered issues the first harvest queue and promises public comment reads without credentials; following subject.uri is the record boundary that makes that contract executable. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- tools/tangled_issues.py | 32 +++++++-- tools/test_tangled_issues.py | 68 +++++++++++++++++++ .../2026-07-27-tangled-comment-read-repair.md | 45 ++++++++++++ ...26-07-27-tangled-issue-client-migration.md | 14 ++++ wiki/log/DEVLOG.md | 5 ++ wiki/process/tick-ledger.md | 2 +- wiki/process/tick.md | 4 ++ 7 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 wiki/log/2026-07-27-tangled-comment-read-repair.md diff --git a/tools/tangled_issues.py b/tools/tangled_issues.py index e3839ec4..5d3566c4 100644 --- a/tools/tangled_issues.py +++ b/tools/tangled_issues.py @@ -35,7 +35,8 @@ PLC_DIRECTORY_BASE = os.environ.get( "MISALIGNED_PLC_DIRECTORY_BASE", "https://plc.directory" ).rstrip("/") ISSUE_COLLECTION = "sh.tangled.repo.issue" -COMMENT_COLLECTION = f"{ISSUE_COLLECTION}.comment" +COMMENT_COLLECTION = "sh.tangled.feed.comment" +COMMENT_SUBJECT_PATH = ".subject.uri" LABEL_DEFINITION_COLLECTION = "sh.tangled.label.definition" LABEL_OP_COLLECTION = "sh.tangled.label.op" DECISION_OPPOSITES = { @@ -100,6 +101,27 @@ def _issue_sort_key(issue: dict[str, Any]) -> tuple[str, str]: return str(issue.get("createdAt", "")), str(issue.get("uri", "")) +def _comment_subject_uri(value: Any) -> str: + if isinstance(value, str): + return value + if isinstance(value, dict): + return str(value.get("uri", "")) + return "" + + +def _comment_body_text(value: Any) -> str: + if isinstance(value, str): + return value + if isinstance(value, dict): + text = value.get("text") + if isinstance(text, str): + return text + original = value.get("original") + if isinstance(original, str): + return original + return "" + + @dataclass(frozen=True) class RecordRef: did: str @@ -401,16 +423,18 @@ class Issues: detail = self.run_tg(["issue", "view", issue["rkey"]]) result = {**issue, **detail} comments = [] - for ref in self.records.backlinks(issue["uri"], COMMENT_COLLECTION, ".issue"): + for ref in self.records.backlinks( + issue["uri"], COMMENT_COLLECTION, COMMENT_SUBJECT_PATH + ): payload = self.records.get(ref) value = payload["value"] - if value.get("issue") != issue["uri"]: + if _comment_subject_uri(value.get("subject")) != issue["uri"]: continue comments.append( { "uri": ref.uri, "authorDid": ref.did, - "body": value.get("body", ""), + "body": _comment_body_text(value.get("body")), "createdAt": value.get("createdAt", ""), } ) diff --git a/tools/test_tangled_issues.py b/tools/test_tangled_issues.py index 5ca98adf..2a99470d 100644 --- a/tools/test_tangled_issues.py +++ b/tools/test_tangled_issues.py @@ -107,6 +107,74 @@ class TangledIssueFixtures(unittest.TestCase): ["decision-required"], [label["name"] for label in listed[0]["labels"]] ) + def test_view_reads_feed_comment_subject_and_markdown_body(self) -> None: + comment_ref = tangled_issues.RecordRef( + OWNER, tangled_issues.COMMENT_COLLECTION, "comment-one" + ) + + class CommentRecords(FakeRecords): + def backlinks( + self, target: str, collection: str, path: str + ) -> list[tangled_issues.RecordRef]: + if collection == tangled_issues.COMMENT_COLLECTION: + self.assert_comment_query = (target, collection, path) + return [comment_ref] + return super().backlinks(target, collection, path) + + def get(self, ref: tangled_issues.RecordRef) -> dict[str, Any]: + if ref == comment_ref: + return { + "uri": ref.uri, + "value": { + "$type": tangled_issues.COMMENT_COLLECTION, + "subject": {"uri": ISSUE_ONE, "cid": "issue-cid"}, + "body": { + "$type": "sh.tangled.markup.markdown", + "text": "1", + "original": "1", + }, + "createdAt": "2026-01-03T00:00:00Z", + }, + } + return super().get(ref) + + records = CommentRecords() + + def run_tg(args: Sequence[str]) -> Any: + if list(args) == ["issue", "list"]: + return [ + { + "rkey": "first", + "uri": ISSUE_ONE, + "createdAt": "2026-01-01T00:00:00Z", + "state": "open", + "title": "First", + } + ] + self.assertEqual(["issue", "view", "first"], list(args)) + return {"body": "Question body", "commentCount": 1} + + detail = tangled_issues.Issues(records, run_tg).view("1") + self.assertEqual( + ( + ISSUE_ONE, + "sh.tangled.feed.comment", + ".subject.uri", + ), + records.assert_comment_query, + ) + self.assertEqual( + [ + { + "uri": comment_ref.uri, + "authorDid": OWNER, + "body": "1", + "createdAt": "2026-01-03T00:00:00Z", + } + ], + detail["comments"], + ) + def test_label_fold_obeys_timestamp_and_delete_operands(self) -> None: records = FakeRecords() records.add_operation( diff --git a/wiki/log/2026-07-27-tangled-comment-read-repair.md b/wiki/log/2026-07-27-tangled-comment-read-repair.md new file mode 100644 index 00000000..d70d7c70 --- /dev/null +++ b/wiki/log/2026-07-27-tangled-comment-read-repair.md @@ -0,0 +1,45 @@ +# Tangled decision comments now cross the real account boundary + +``` +Type: log +``` + +## Finding + +The newly centralized issue wrapper could list issue #15 and report Tangled's +`commentCount: 1`, but its returned `comments` array was empty. The helper +queried a nonexistent issue-local comment collection and path, so the highest +priority tick queue could recognize that Cameron had decided without reading +what he chose. + +## Repair + +Tangled comments are `sh.tangled.feed.comment` records owned by the commenter, +not the repository owner. Constellation indexes the strong-reference backlink +at `.subject.uri`; the returned record's markup body carries its plain text at +`body.text`. + +`tools/tangled_issues.py view` now follows that exact backlink, fetches each +record from the author's PDS, validates that the subject still names the issue, +and projects the plain body text in stable chronological order. The helper +retains bounded compatibility reads for a direct string subject or body but +does not scan one known account or fabricate an empty thread. + +The fixture fails if the collection, backlink path, strong-reference read, or +markup-body read regresses. A live issue #15 view now returns Cameron's comment +`1` at its canonical AT-URI, restoring the decision harvest that this tick +needed. + +## Defense + +`wiki/process/tick.md` makes answered decisions the first work queue and says +public comment reads require no credential. That promise needs a cross-account +record query because comments live with their authors. An issue-owner PDS scan +or empty-array fallback would silently discard the very decision the queue is +supposed to harvest. + +## Verification + +- `python3 tools/test_tangled_issues.py` +- live `python3 tools/tangled_issues.py view 15`, reduced to its number, rkey, + count, and returned comments for inspection diff --git a/wiki/log/2026-07-27-tangled-issue-client-migration.md b/wiki/log/2026-07-27-tangled-issue-client-migration.md index 40433d6d..0dab0b0c 100644 --- a/wiki/log/2026-07-27-tangled-issue-client-migration.md +++ b/wiki/log/2026-07-27-tangled-issue-client-migration.md @@ -55,3 +55,17 @@ addressing. - tick brief renders `decision-made` before `decision-required`; - project status consumes structured issue objects rather than parsing prose; - doctor reports the Go client plus public record boundary healthy. + +## Follow-up: comment records + +The first landing used a nonexistent issue-local comment collection and +therefore returned an empty `comments` array even when `tg issue view` reported +one reply. Tangled comments actually live as `sh.tangled.feed.comment` records +on the comment author's PDS. Their strong-reference `subject.uri` points to the +issue, and their markup body carries player text under `body.text`. + +The helper now follows Constellation's `.subject.uri` backlink, reads the +comment from its author's PDS, validates the exact subject, and returns the +plain markup text. A fixture pins that collection, link path, strong reference, +and body shape. The live read of issue #15 now returns Cameron's recorded +choice `1` rather than a false empty thread. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 9ee08865..55af13d7 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -21,6 +21,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-27-tangled-issue-client-migration.md](2026-07-27-tangled-issue-client-migration.md) +## 2026-07-27 - Tangled decision comments now cross the real account boundary + +- Intent: (see session log) +- Log: [wiki/log/2026-07-27-tangled-comment-read-repair.md](2026-07-27-tangled-comment-read-repair.md) + ## 2026-07-27 - The Tangled CLI section, verified against the machine - Intent: (see session log) diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 5891d25f..9e8e8120 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -20,7 +20,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | Slice | Last audited | Verdict | Trace | |---|---|---|---| -| `wiki/process/tick.md` + issue automation | 2026-07-27 | finding | the queued retired-client violation held across binding procedure, both skill mirrors, four active prompt templates, tick intake, project status, and doctor. `tools/tangled_issues.py` now keeps repository discovery and authenticated issue writes on canonical Go `tg`, reconstructs deterministic display numbers from public issue records, folds public label-op history, and provides fail-closed structured list/view/create/edit/comment/close/label operations. The process surfaces consume that one boundary, helper fixtures pin ordering, label folds, decision-label exclusion, validation, and retired-client absence, and live reads resolve the six current open issues plus their labels — [log](../log/2026-07-27-tangled-issue-client-migration.md) | +| `wiki/process/tick.md` + issue automation | 2026-07-27 | finding | the queued retired-client violation held across binding procedure, both skill mirrors, four active prompt templates, tick intake, project status, and doctor. `tools/tangled_issues.py` now keeps repository discovery and authenticated issue writes on canonical Go `tg`, reconstructs deterministic display numbers from public issue records, folds public label-op history, and provides fail-closed structured list/view/create/edit/comment/close/label operations. A same-day live harvest exposed one wrong record assumption: comments are cross-account `sh.tangled.feed.comment` records linked through `subject.uri`, not issue-local child records. The repaired helper follows that backlink into the author's PDS and returns the markup text; a fixture pins the exact shape, and live issue #15 now returns Cameron's choice `1` — [migration and repair log](../log/2026-07-27-tangled-issue-client-migration.md) | | `wiki/world/places/basement-map.md` room topology | 2026-07-27 | finding | the queued five non-hall corridor cuts remained, and an executable all-room perimeter audit exposed the same dead-door shape at the loading-dock entry plus fixed objects blocking the interior faces of the roll door, HVAC door, both storage doors, Janitor door, and stairwell. Every non-hall approach now reaches its authored door from outside the prefab, every other perimeter tile remains closed, doorway interiors are clear, and the roll, sealed, tier-2, and tier-3 boundaries retain their exact kinds — [room-approach log](../log/2026-07-27-room-approaches-meet-doors.md). Prior [west-approach](../log/2026-07-26-west-hall-approach.md) and [hall-density](../log/2026-07-26-foundation-hall-density.md) repairs stand. | | `wiki/mechanics/sensor-network.md` sensor population | 2026-07-26 | finding | Cameron could only ever see one pool of light. Cause was not coverage shape but inventory: the whole B1 plate authors three sensing devices (hall monitor, security's dock camera, one tier-3 stairwell node), so nothing remains to acquire after the Eyes beat and sight cannot grow. Captured the reframe — sensors are ambient infrastructure (~30 in B1, populated by rule), access is the scarce thing, darkness always traces to a nameable air gap, and a new human operator room is watchable only through a player-installed sensor. Also found that reach.md already specifies the per-tick subscription drain and UNTAP that make curation a skill; it was simply unreachable with three sensors, so this supplies content to an existing economy rather than adding one. Corridors and Crawlspace prefabs still absent, recorded as basement-map residue — [log](../log/2026-07-26-sensor-network-capture.md) | | `wiki/interface/keymap.md` + terminal/Bevy input routes | 2026-07-26 | finding | the canonical table assigned `A` to left movement and only `e` / Enter to the context menu, but terminal still opened and closed menus with its older `a` alias and lacked the specified Shift+direction semantic jump. Terminal now implements WASD parity, `a` means left, `e` / Enter alone open the menu, and both frontends consume one renderer-neutral nearest-earned-anchor query without changing selection or opening a menu. README, action-vocabulary, terminal, context-menu, and pinned terminal hints now teach the same boundary — [log](../log/2026-07-26-terminal-keymap-a-reconciliation.md) | diff --git a/wiki/process/tick.md b/wiki/process/tick.md index c20536e4..aa9eedf1 100644 --- a/wiki/process/tick.md +++ b/wiki/process/tick.md @@ -83,6 +83,10 @@ authenticated issue write to `tg`, then joins the public current client does not expose. The helper emits structured JSON for agents. Numeric `#N` values are deterministic display addresses reconstructed from issue creation order; rkeys and AT-URIs are the durable automation handles. +Public comments are `sh.tangled.feed.comment` records on each comment +author's PDS whose `subject.uri` points back to the issue; the helper follows +that cross-account backlink rather than assuming comments live with the +repository owner. ```bash python3 tools/tangled_issues.py list --state open -- 2.51.2