Conversation
…#912) Docs and learnings are already scoped by role/project namespace (Tencent#707); teamwiki was not. A checkout active on one project (`--project svc-a`) got `teamai recall` hits from every codebase's `evidence/code/<slug>/`, including ones that belong to an unrelated project — recall would surface another team's codebase evidence just because it lived in the same wiki. Adds a hand-declared `wiki` resource type (manifest-schema.ts), following the exact pattern `docs` already uses: - `HAND_DECLARED_RESOURCE_TYPES` gains `'wiki'` — this alone threads it through the existing generic plumbing in roles.ts/projects.ts (NAMESPACED_RESOURCE_TYPES, mergeNamespaces, resolveRoleResourceNamespaces, resolveProjectResourceNamespaces all already loop over HAND_DECLARED_RESOURCE_TYPES generically) with no changes needed there. - resource-namespaces.ts computes `inactiveWikiNamespaces` the same way it already computes `inactiveDocsNamespaces`: a slug ANY role or project declares under `resources.wiki` is withheld unless this directory's active role/project selects it; an undeclared slug stays shared. - code-knowledge-recall.ts's `loadWikiPages`/`queryCodeKnowledge` take a new `withheldCodebases` option and filter `evidence/code/<slug>/` directories by it (case-folded, matching how the directory lookup itself is effectively case-insensitive on Windows/macOS). - recall.ts wires the two together: computes `withheldCodebases` from the active project config via `resolveResourceNamespaces` and passes it to `queryCodeKnowledge`. The slug is whatever `teamai codebase --project <slug>` wrote, unrelated to a manifest project id, so a team declares the one it already uses — no new convention needed. Docs: usage-guide.md (+ zh-CN) and the admin skill reference (manage-admin.md) each get a short "Wiki by namespace" paragraph mirroring the existing "Docs by namespace" one. Test plan: 12 new tests across 3 files — resource-namespaces-wiki.test.ts (inactiveWikiNamespaces resolution against real roles.yaml/projects.yaml fixtures), code-knowledge-recall-wiki-scope.test.ts (withheldCodebases filtering against real teamwiki fixtures, including case-fold matching), recall-wiki-scope.test.ts (recall()'s wiring, asserting the exact withheldCodebases argument queryCodeKnowledge receives). All 12 pass. Typecheck and lint (oxlint --type-aware) both clean. The 5 existing test files most directly touched by this change (recall.test.ts, recall-scope-isolation.test.ts, pull-docs-namespaces.test.ts, resource-namespaces-case-alias.test.ts, projects.test.ts — 71 tests) all still pass with zero regressions. A full project-wide suite run was attempted but the host machine was resource-starved from this session's own accumulated test-run processes (100+ orphaned node.exe, causing cascading 15s test timeouts and one literally-impossible "8514000ms" timer reading); killed rather than trust its output. recall-attribution.test.ts's one slow/timeout-prone test was independently confirmed pre-existing and unrelated via direct git-stash before/after comparison. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
…iki namespace (Tencent#974 review) codex-review on PR Tencent#974 found two gaps where a withheld codebase still leaked through recall despite the page-level scoping: - `--depth route` returned the global `teamwiki/router.md` verbatim. Since that file lists every codebase (name, link, description, keywords) plus a `<!-- search-anchor -->` comment aggregating all keywords, a directory scoped to one codebase could still see every other one's entry. Fixed by stripping withheld-codebase bullet lines and the anchor comment from the content before it's returned. - The knowledge graph (`.indices/graph-index.json`) was loaded unfiltered at `context`/`lookup` depth, so a withheld codebase's nodes could still match as BM25 entry nodes, boost scores via graph neighbors, or — most concretely — surface a withheld codebase's own node identifier through an allowed page's `relatedFiles` via a cross-repo DEPENDS_ON edge. A withheld codebase's fact-level graph nodes/edges (AST/heuristic) are keyed by raw file path with no `evidence/code/<slug>/` prefix at all, so there's no reliable string to filter the already-merged graph by after the fact. Fixed at the root instead: `graph-aggregate.ts`'s existing per-repo aggregation (the one place that still knows, per per-repo graph file, which codebase it came from) now accepts an `excludeProjects` set and skips a withheld codebase's graph file entirely — including skipping cross-repo edge detection against it. `queryCodeKnowledge` calls this scoped rebuild instead of reading the flat merged file whenever any codebase is withheld. Verified via the real built CLI (`npm run build` + a hand-built team repo + project fixture), not just unit tests: - `recall "octopus" --depth lookup` with only `svc-a` active returns only svc-a's page; with both active, both return. - `recall "router" --depth route` with only `svc-a` active strips svc-b's bullet and the keyword anchor from router.md's snippet. - A synthetic cross-repo graph edge (`a/client -> b/service`) surfaces `b/service` in "Candidate change files" when both projects are active, and disappears when svc-b is withheld. Added 7 new tests (3 in graph-aggregate.test.ts for buildAggregatedGraph's excludeProjects, 4 in code-knowledge-recall-wiki-scope.test.ts for router filtering and cross-codebase relatedFiles leakage) plus the 12 from the original PR. All 39 tests across every touched/related file pass, typecheck and lint are clean. A full project-wide `npx vitest run` was killed by the harness due to low system memory (not a test failure) before completing; given the same resource constraint as the original PR, this round relies on the targeted evidence above rather than a full clean run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed all three P1 findings in 066c8d7:
Real-CLI verification (not just unit tests)Built (
TestsAdded 7 new tests (3 for One caveat, same as the original PR: a full project-wide |
The previously reported route-router and graph-index isolation findings are resolved in the current diff. |
…admin docs (Tencent#974 review) codex-review's second pass on Tencent#912 found `wiki` missing from every enumeration of the hand-declared resource types outside manifest-schema.ts itself, which is the one place it had actually been added: - manage-admin.md's "every member upgrade before declaring..." warning and its directory-naming-rule list both still stopped at `docs`. An admin following the guide could declare `resources.wiki` before every member upgraded, and an older CLI rejects the unknown key and stops that member's pull. - `teamai projects add --namespaces`'s help text (and the generated `commands.md`, regenerated via `npx vitest run commands-reference -u`) and docs/designs/multi-project-management.md's resource-type enumeration also still read "env, hooks, mcp, models and docs". Also moved the real-CLI verification record from a PR comment into the PR description itself, per Code Review Rules (the description must document sufficient testing for a runtime-behavior change, not a comment on it). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed in c967370:
Typecheck, lint, and the |
The previously reported testing-record and documentation/help propagation gaps are resolved in the current PR description and diff. |
…al-only graph edges (Tencent#974 review round 2) codex-review's third pass found the first graph/router fix (round 2) had two real gaps and one unrelated robustness issue: - Router filtering only recognized `[[evidence/code/<slug>/index]]` links (routerTemplate's format), but `rebuildWikiIndex()` — the path actually taken after an import — writes `[[code/<slug>/index]]` table rows instead, with no `evidence/` prefix. A checkout scoped away from a codebase imported this way still saw its full row (domain, name, duty, keywords) at `--depth route`. Fixed by widening the line-match regex to accept an optional `evidence/` prefix before `code/<slug>`. - The round-2 graph fix rebuilt the scoped graph from only the allowed per-repo graph files, which discards anything that exists ONLY in the global `.indices/graph-index.json` — chiefly `teamai codebase --reconcile`'s product<->code MAPS_TO edges, since the reconciler reads and writes the global graph directly and never touches a per-repo file. Activating any wiki restriction therefore silently dropped those mappings even for codebases that were still fully allowed. Fixed by flipping the approach: `scopeGlobalGraph` (replacing `buildAggregatedGraph`'s `excludeProjects`) now starts from the real global graph and *subtracts* exactly the withheld codebase's own node slugs (read from its per-repo file, which still correctly names every fact-level node it contributed, prefixed or not), then drops any edge left dangling from a removed endpoint. That dangling-edge cleanup is what still removes a cross-repo DEPENDS_ON edge into a withheld node — the original leak this whole graph-scoping effort started from — without needing to know the edge came from cross-repo detection rather than a per-repo file. - (P2) `resolveResourceNamespaces()` in recall.ts ran outside the existing code-retrieval try/catch, so an unreadable or malformed roles/projects manifest rejected the whole recall call even when a valid learnings index had already been searched safely. Moved it inside the same try, so it now degrades the same way a `queryCodeKnowledge` failure already does: skip code recall, warn, keep the rest of the results. Also moved the real-CLI verification record into the PR description in the previous round, per Code Review Rules; this round's new real-CLI checks are documented there too. Verified via the real built CLI again: a `router.md` fixture in rebuildWikiIndex's actual table-row format now has the withheld row stripped at `--depth route`; a synthetic global-only forward edge (mimicking what --reconcile adds) survives scoping in "Candidate change files" while a cross-repo edge into the withheld codebase still doesn't; and a deliberately malformed `projects.yaml` now degrades to the existing "code graph retrieval unavailable" warning (exit 0) instead of crashing recall. 9 tests added/rewritten (7 in graph-aggregate.test.ts for scopeGlobalGraph, replacing the 3 excludeProjects tests it supersedes; 1 router table-row test and 1 global-only-edge-preservation test in code-knowledge-recall-wiki-scope.test.ts; 1 malformed-manifest regression test in recall-wiki-scope.test.ts). All 129 tests across every touched/related file pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed all three findings in 070e8fd:
Real-CLI re-verification
Tests9 tests added/rewritten (7 replace the superseded PR description updated with the full round-2 verification record. |
The earlier router-format, manifest-error, help/design, and real-CLI testing findings are resolved; the PR description now includes sufficient representative real-CLI verification. |
…nish wiki doc propagation (Tencent#974 review round 3) codex-review's fourth pass found scopeGlobalGraph itself (round 2's fix) had a real over-pruning bug, a real-but-smaller ambiguity, and three more stale doc enumerations: - [P1] The edge filter required BOTH endpoints to survive as nodes in the scoped graph (`keptSlugs.has(e.from) && keptSlugs.has(e.to)`). But an AST/heuristic edge is commonly file-to-file (`{from: 'src/a.ts', to: 'src/b.ts'}`) with neither endpoint present in `nodes[]` at all — so the moment anything was withheld, every such edge vanished for every codebase, allowed ones included, dropping their files from `relatedFiles`/"Candidate change files". Fixed by subtracting by identity instead: collect every node slug AND edge endpoint a withheld codebase's own per-repo file contributed, then drop only edges actually touching one of those — not edges that merely fail to resolve to a known node. - [P2] Fact-level node slugs are not repo-qualified (`buildCodeGraph` mints `component/App` the same way for any repo), so an allowed and a withheld repo can legitimately collide on one slug after merging; subtracting by slug alone would also remove the allowed repo's node. Fixed by also collecting every ALLOWED codebase's own identifiers and clearing any that overlap with the withheld set before subtracting — a shared identifier is never removed. - [P2] `docs/usage-guide.md` (+ zh-CN) still said `--namespaces` "neither touches env, hooks, mcp, models or docs", omitting `wiki`, and `CHANGELOG.md`'s Tencent#707 entry enumerating `resources:` keys was stale the same way. Added `wiki` to all three. Verified via the real built CLI again: a file-to-file edge with neither endpoint declared as a node now survives in "Candidate change files" even when an unrelated codebase is withheld (previously it vanished the moment anything was withheld, regardless of relation). Added 2 tests to graph-aggregate.test.ts for the two scopeGlobalGraph bugs (file-to-file edge preservation, slug-collision non-removal) and fixed a test-only bug in the existing cross-repo-edge test (it filtered by `relation === 'DEPENDS_ON'`, which also matches the surviving, unrelated import edge after `loadGraphIndex` normalizes the legacy `imports` relation — switched to filtering by the actual withheld endpoint). 132 tests total across every touched/related file pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed both findings in 5ccde9f:
Real-CLI re-verificationA file-to-file edge with neither endpoint declared as a node (the real shape of an AST/heuristic edge) now survives in "Candidate change files" even when an unrelated codebase is withheld — previously it vanished regardless of relevance the moment anything was withheld. Tests2 new tests in PR description updated with the full round-3 record. |
The previously reported testing, documentation, malformed-manifest, router-link-format, raw-edge, and duplicate-slug findings are resolved in the current diff. |
…dded withheld code-page nodes (Tencent#974 review round 4) codex-review's fifth pass found two more real leaks, both in the graph/router scoping this PR already added: - [P1] Router filtering only removed individual lines carrying a `code/<slug>` link. `routerTemplate()`'s AI-domain branch, though, emits a `### <domain>` header line with no link at all, and an unresolved component (one whose name does not match any known project) as a bare `- <name>` line with no link either. Once the properly-linked sibling lines for a withheld codebase were removed, an all-withheld domain's header — and any such unlinked fallback line under it — survived untouched, still naming the domain and component. Fixed by grouping router.md into sections at each markdown header first: a section whose links are ALL withheld (and at least one was found) is now dropped whole, header and any unlinked lines included; a section mixing allowed and withheld links still keeps its header and drops only the withheld lines, as before. - [P1] `scopeGlobalGraph` identified withheld content solely from per-repo graph files, but `teamai codebase --reconcile` adds code-page nodes (`evidence/code/<slug>/<page>`) and their MAPS_TO edges straight to the global graph, the same way it adds product-page nodes — never to a per-repo file. After reconciling and then withholding that codebase, those nodes/edges survived, so a query matching the withheld page's title could still use it as an entry node or graph boost. Fixed with a second pass over the global graph's own nodes, matched by the `evidence/code/<slug>/` prefix (which — unlike a bare fact-level slug — unambiguously names the codebase that owns it, so no allowed/withheld collision risk the way there is for prefix-less slugs). Verified via the real built CLI: an all-withheld domain section (header + unlinked fallback line) is now fully gone from `--depth route`'s snippet while a mixed/allowed domain's header and allowed line survive; a `scopeGlobalGraph` call against a global graph with an injected reconcile-style withheld code-page node and MAPS_TO edge confirms both are removed while an unrelated product-page node survives. 4 tests added (2 router domain-section tests in code-knowledge-recall-wiki-scope.test.ts; 1 reconcile-added-node test in graph-aggregate.test.ts). 135 tests total across every touched/related file pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed both findings in b564757:
Real-CLI re-verification
Tests4 tests added (2 router domain-section tests, 1 reconcile-added-node test, consistent with the existing suite's patterns). 135 tests total pass; typecheck and lint clean. PR description updated with the full round-4 record. |
The previously reported router filtering, reconcile-added node filtering, documentation propagation, malformed-manifest handling, and real-CLI testing gaps are resolved. The PR description now documents sufficient testing. |
…ope colliding-slug edges (Tencent#974 review round 5) codex-review's sixth pass found scopeGlobalGraph still failed OPEN in one real scenario, plus a narrower P2 it had already flagged as needing repository-aware edge handling: - [P1] `teamai codebase --extract` run directly (not through `teamai import`'s cache-then-copy orchestration) writes only the global `teamwiki/.indices/graph-index.json` and never populates `evidence/code/<slug>/.indices/graph-index.json` for that codebase. scopeGlobalGraph's withheld-identifier collection only reads that per-repo path, so a codebase extracted this way had NO identifiers collected for it at all when withheld — its fact-level nodes and edges stayed in the "scoped" graph fully exposed, able to affect BM25 entry-node matching, graph-boost scoring, or `relatedFiles`. Fixed by failing closed instead of open: if any withheld codebase's per-repo graph file is missing or unreadable, scopeGlobalGraph now returns `null` (no graph at all for this query) rather than a result it cannot vouch for. `queryCodeKnowledge` already treats a `null` graph as "skip graph-based features," the same as when no graph exists at all, so this needed no change on the consumer side. - [P2] Clearing an identifier shared between an allowed and withheld repo (round 3's fix for colliding fact-level slugs like `component/App`) correctly keeps both repos' nodes, but a withheld repo can also have an EDGE directly between two such colliding names — a relationship that only ever existed in the withheld repo, which neither endpoint-identifier removal nor the "shared identifier survives" rule would catch, since both endpoints end up allowed. Fixed by tracking edge pairs (`from|to`) the same way identifiers are tracked: a withheld-only pair is subtracted even when both of its endpoints individually survive; a pair also present in an allowed repo's own graph is still preserved. Verified via the real built CLI: a codebase with evidence pages but no per-repo graph file (simulating a direct `--extract`) makes an unrelated, fully-accounted-for codebase's own graph-derived "Candidate change files" entry disappear too the moment the no-graph-file codebase is withheld — confirming the fail-closed behavior applies to the whole query, not just the unaccounted codebase. 3 tests added to graph-aggregate.test.ts (fail-closed on a missing per-repo file; does NOT fail closed when only an allowed, unrelated codebase lacks one; the colliding-edge subtraction). 138 tests total across every touched/related file pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed both findings in 200c1c6:
Real-CLI re-verificationA codebase with evidence pages but no per-repo graph file (simulating a direct Tests3 tests added to PR description updated with the full round-5 record. |
The previously reported missing-graph fail-open issue is resolved, and the PR description now includes sufficient representative real-CLI verification. |
…ding node's metadata (Tencent#974 review round 6) codex-review's seventh pass found a real P1 in router filtering that survived the round-4 fix, a real P1 in the colliding-slug handling added in round 5, a related P2, and a stale doc claim: - [P1] A mixed router domain section (some links allowed, some withheld) correctly dropped withheld-linked lines, but a bare unresolved-component fallback line (`routerTemplate`'s "no project match" branch, no link at all) survived regardless, since `lineSlug()` can never attribute an unlinked line to either side. `filterRouterContent` only ever runs when something IS withheld, so that ambiguity can't be resolved safely either way — fixed by dropping every unlinked bullet line unconditionally, the same fail-closed-on-ambiguity rule `scopeGlobalGraph` already applies to graph ownership it cannot verify, rather than trusting an unlinked line is innocent just because an allowed link sits nearby. - [P1] Clearing a slug an allowed repo also claims (round 5's fix for cross-repo slug collisions) correctly kept the node, but never corrected its DATA: `mergeGraphs` lets the later-processed repo's node win outright with no field-level merge, so the surviving global node could still carry the withheld repo's title/domain if that repo's write happened to win. Querying that title could then select it as an entry node and affect scoring. Fixed by re-attaching the allowed repo's own copy of a contested node (read in the same per-repo scan already in place) so its metadata is actually attributable to an allowed source. Required also tracking which slugs were contested BEFORE the allowed set clears them out of the withheld set, since the existing "nothing to remove" fast path would otherwise skip this restoration too. - [P2] The edge-ownership key omitted `relation`, so an allowed `App -REFERENCES-> Config` cleared a withheld `App -DEPENDS_ON-> Config` out of the withheld set even though it never claimed that specific relation — both edges then survived. Fixed by keying edges on `from|to|relation`, matching the graph schema's own edge identity (`graphEdgeKey` in graph-index.schema.ts already does the same). - [P2] `usage-guide.md` (+ zh-CN) said wiki scoping needs a team repo "with manifest/projects.yaml", but a role-only declaration in manifest/roles.yaml works with no projects.yaml at all (and the rest of the same sentence already said "any role or project"). Reworded to not imply projects.yaml is required. Verified via the real built CLI and a direct scopeGlobalGraph call: a mixed router domain's unmatched component name is now stripped alongside the allowed line staying; a single scopeGlobalGraph call against a graph where the withheld repo's node metadata won the merge now returns the allowed repo's title/domain for the shared slug AND keeps only the allowed REFERENCES edge between the colliding endpoints, dropping the withheld DEPENDS_ON one. 4 tests added (1 router mixed-domain bare-line test; 2 graph tests for metadata restoration and relation-aware edge keys — the metadata test caught the fast-path bug above on its first run). 141 tests total across every touched/related file pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed all four findings in 5bd9f5f:
Real-CLI re-verification
Tests4 tests added (1 router mixed-domain bare-line test, 2 graph tests — the metadata-restoration test caught the fast-path bug on its first run before I fixed it). 141 tests total pass; typecheck and lint clean. PR description updated with the full round-6 record. |
The earlier findings are resolved in the current diff, and the PR description now includes sufficient representative real-CLI verification. |
…ions before scoping (Tencent#974 review round 7) codex-review's eighth pass found two more real P1s in scopeGlobalGraph and a cheap robustness nit: - [P1] A withheld codebase's per-repo graph file was marked "accounted for" the moment JSON.parse succeeded, with no check that the result was actually a graph. A truncated or otherwise corrupted write can leave syntactically valid JSON (`{}`) that is not a graph at all — `nodes ?? []` / `edges ?? []` would silently contribute zero identifiers while the global graph still has that codebase's real content, so scoping would run as if there were nothing to subtract. Fixed by validating the parsed result has `nodes`/`edges` arrays before marking it accounted for; anything else now falls into the same fail-closed path as an unreadable file. - [P1] Per-repo-file edge-ownership keys used the raw relation string, but `loadGraphIndex` normalizes the legacy `imports` relation name to `DEPENDS_ON` when it loads the global graph. scopeGlobalGraph reads per-repo files with a plain JSON.parse, bypassing that normalization — so a withheld repo's own `imports` edge computed a key that could never match the already-normalized `DEPENDS_ON` edge in the loaded global graph, and that withheld relationship would survive. Fixed by normalizing the relation the same way before computing the key (exported `LEGACY_RELATIONS` from graph-index.schema.ts rather than duplicating the mapping). - [P3] The edge-ownership key concatenated `from|to|relation` with a plain `|`, which a `|`-containing identifier could alias (`a|b -> c` vs `a -> b|c`). Switched to `JSON.stringify([from, to, relation])`, matching `graphEdgeKey`'s own approach in graph-index.schema.ts. Verified via the real built code: a structurally-invalid-but-valid-JSON per-repo file (`{}`) for a withheld codebase now makes scopeGlobalGraph return null instead of silently treating it as nothing-to-subtract; and a withheld edge recorded under the legacy `imports` relation name is now correctly normalized and removed, leaving only the allowed REFERENCES edge between the same colliding endpoints. 2 tests added to graph-aggregate.test.ts (structural-validation fail-closed; legacy-relation-name normalization). 143 tests total across every touched/related file pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed all three findings in c04c19f:
Real-CLI re-verification
Tests2 tests added to PR description updated with the full round-7 record. |
|
Findings
The previously reported router filtering, documentation/help propagation, malformed-manifest handling, edge-key normalization, and real-CLI testing-record issues are resolved. |
…se shape check (Tencent#974 review round 8) codex-review's ninth pass found the round-7 fix for an unvalidated per-repo graph file was itself too shallow, which also explained a second P1 in the metadata-restoration path: - [P1] The "structural validation" added last round only checked that `nodes`/`edges` were arrays. A node that doesn't conform to GraphNodeSchema (missing required fields, wrong types) still passed that check and was silently skipped (no slug extracted, contributing nothing) while counting the whole file as "successfully read ownership from" — so a withheld codebase with any schema-invalid nodes mixed into real ones could still leave those real nodes exposed in the global graph. Fixed by reusing `parseGraphIndex` (now exported from graph-index.schema.ts) — the exact same schema `loadGraphIndex` validates the global graph with — instead of a loose array check. Anything that doesn't validate now falls into the same fail-closed path as an unreadable file. - [P1] Restoring a contested node's metadata (round 6's fix) spread the RAW per-repo JSON object over the surviving global node. For a node using the legacy `id`/`label`/`kind` shape, the raw object has no `title` key at all (only `label`) — so the spread set `id`, `label`, `kind` but never actually overrode `title`, leaving the withheld repo's title in place if its write had won the merge. Fixed by the same `parseGraphIndex` reuse: it already normalizes `label`→`title`, `id`→`slug`, `kind`→`type` the same way the global graph itself was normalized on load, so the node now stored for restoration carries the right field names, not just the right file. Reusing one validator for both problems let the manual relation normalization added last round (`normalizeRelation`) come out too — `parseGraphIndex`'s edge schema already normalizes the legacy `imports` relation name, so a second, hand-rolled copy of that mapping is no longer needed. Verified via the real compiled code: a withheld codebase's per-repo file with syntactically-valid-but-schema-invalid nodes now makes scopeGlobalGraph return null; and restoring a contested node whose allowed repo used the legacy label field now correctly shows that repo's title, not the stale one left by whichever repo's write won the merge. 2 tests added to graph-aggregate.test.ts (schema-invalid node fail-closed; legacy-field-normalized restoration). 145 tests total across every touched/related file pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…erring it from per-repo files at query time (Tencent#974 review round 10) codex-review's eleventh pass pointed out that every per-repo-file-based fail-closed guard added over the last several rounds (missing, unreadable, schema-invalid, empty) still shares one root assumption: that a withheld codebase's CURRENT per-repo file completely and accurately describes everything it ever contributed to the global graph. An interrupted `teamai import` breaks exactly that assumption — it can replace a withheld codebase's per-repo graph with a different, valid, non-empty one and be interrupted before the next aggregateGlobalGraph() call folds the change in, leaving the global graph holding OLD nodes/edges that no longer appear anywhere in the per-repo file scopeGlobalGraph would read. No amount of validating that file harder closes this, since the file can be perfectly valid and just describe something else now. Fixed at the root instead, per the review's own suggestion: ownership is now stamped onto the data itself at the moment it is merged. `buildAggregatedGraph` tags every node and edge from a per-repo file with `origin: <that codebase's slug>` before merging it in (`origin` is now a first-class optional field on `GraphNode`/`GraphEdge`, not a passthrough accident). That tag travels with the data in the global graph forever after — correct regardless of whatever the per-repo file is later rewritten to, emptied to, or deleted to. `scopeGlobalGraph` now checks tags first: a withheld codebase with ANY `origin`-tagged content in the global graph is "tag-covered," and its node/edge removal is read directly off those tags — no per-repo file read at all. Only a codebase tagging has never covered (predates this field, or was extracted directly via `teamai codebase --extract` outside `teamai import`'s per-repo-file-producing orchestration) falls back to the existing per-repo-file mechanism, with all of its existing fail-closed guards intact for exactly that narrower, legacy case. Allowed codebases are still always read regardless of tagging, since collision handling (two codebases sharing one unqualified fact-level slug) needs an allowed codebase's own data to restore — the merged graph keeps only one node per colliding slug, so tagging alone can't tell "this slug is ALSO legitimately an allowed codebase's." Verified via the real built CLI and a direct scopeGlobalGraph call reproducing the review's exact scenario: aggregate once (tagging svc-b's node), replace svc-b's per-repo file with a newer, valid, non-overlapping graph with no re-aggregation, then confirm the OLD tagged node is still correctly removed — without the function ever needing to read (or being misled by) the replaced file. 3 of the existing per-repo-file fail-closed tests now assert the strictly better outcome this enables (precise scoping instead of failing the whole query closed, since tagging already covers those cases) rather than being weakened; a 4th, reproducing the review's exact interrupted-reimport scenario, is new, alongside a kept regression test confirming the per-repo-file fallback still fails closed for a codebase tagging has never covered. 148 tests total across every touched/related file pass; typecheck and lint are clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed in b569a02 — this one went to the root cause rather than another patch. Confirmed real and correctly identified as the common thread: every per-repo-file fail-closed guard from the last several rounds (missing, unreadable, schema-invalid, empty) shared one assumption — that a withheld codebase's current per-repo file completely describes everything it ever contributed to the global graph. An interrupted Fix: took your second suggested option — ownership is now stamped onto the data at merge time, not re-inferred from a separate, independently-mutable file at query time. Collision handling (two codebases sharing one unqualified slug) still reads an allowed codebase's own file regardless of tagging — tagging alone can't express "this slug is also legitimately someone else's," since the merged graph keeps only one node per slug. Real-CLI re-verificationReproduced your exact scenario directly: aggregate once (tagging svc-b's node), replace svc-b's per-repo file with a newer, valid, non-overlapping graph, skip re-aggregation, then confirm the OLD tagged node is still correctly removed from a real Tests3 of the existing per-repo-file fail-closed tests now assert the strictly better outcome tagging enables (precise scoping instead of failing the whole query closed) rather than being weakened; a 4th reproduces the interrupted-reimport scenario exactly; a kept test confirms the fallback still fails closed for a codebase tagging has never covered. 148 tests total pass; typecheck and lint clean. PR description updated with the full round-10 record. |
|
Findings
The prior stale-generation issue is resolved by aggregation-time origin tagging. The PR description includes sufficient representative real-CLI verification. |
…es (Tencent#974 review round 11) `teamai codebase --reconcile` repairs a code-ast/code-heuristic edge whose endpoint isn't a node yet by minting one, but never copied the edge's own `origin` tag onto it. A withheld codebase's tagged edges got removed from scopeGlobalGraph's output, but these origin-less placeholder nodes stayed behind and searchable, letting withheld content affect entry-node matching and graph boosts. Both endpoints of an intra-repo AST/heuristic edge belong to the same codebase that produced it, so the fix stamps `origin: edge.origin` on the node at the moment it's minted — consistent with round 10's "tag ownership at creation, not inferred later" approach. scopeGlobalGraph needs no changes: its existing origin-tag pass already removes a tagged node. Also fills the P2 doc gap: product-overview.md's resource-namespace summary (and its zh-CN counterpart) covered docs scoping but never mentioned that the Team Context codebase graph is scoped the same way via resources.wiki. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks — confirmed, both findings are real. P1 (graph-aggregate.ts:214 / knowledge-reconciler.ts). Root cause was in the reconciler, not in the Fixed at the source rather than adding another branch to Added P2 (docs). Added a one-line cross-reference from 150 tests passing (51 new/rewritten), typecheck and lint clean. Pushed as |
|
Findings
The previously reported originless repaired-endpoint issue is resolved. The PR description includes sufficient representative real-CLI verification. |
… catch originless withheld edge endpoints (Tencent#974 review round 12) Two gaps in scopeGlobalGraph's origin-tag removal, both P2: - detectCrossRepoEdges synthesizes an edge spanning two codebases and left it fully untagged. If the matched node's unqualified slug later collided with an allowed repo's (last write wins the merge), the edge survived scoping even though it was only ever detected because of the withheld repo's component. Fixed by tagging the edge with the origin of whichever side was matched by label lookup at detection time (captured before any later collision can reattribute it), not re-derived from the final graph. - The evidence/code/<slug>/ prefix pass that catches --reconcile-added content only scanned node slugs. import-iwiki.ts's own reconciler can write a MAPS_TO edge straight to the global graph pointing at a code page path with no node ever created for it, so that edge's endpoint was never caught. Extended the same prefix check to edge endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks — both P2 findings are real, confirmed and fixed. graph-aggregate.ts:201 (cross-repo edge survives a slug collision). Fixed by tagging the edge, at the exact moment graph-aggregate.ts:292 (originless edge endpoint from import-iwiki.ts). Confirmed — the prefix pass only scanned Added regression coverage for both: 158 tests passing (55 new/rewritten), typecheck and lint clean. Pushed as |
|
Findings
The previously reported originless-edge endpoint gaps are resolved. The PR description includes sufficient representative real-CLI verification. |
…tched side (Tencent#974 review round 13) Round 12 tagged a synthesized cross-repo edge with only the origin of whichever side detectCrossRepoEdges matched by label lookup. But the relationship depends on BOTH sides: the repo whose own import produced the match, and the repo that was matched. If the OTHER side's unqualified slug later collided with a third, allowed repo (last write wins the merge), the edge had no tag left pointing at the real withheld repo and survived. Replaced the single-valued origin tag on cross-repo edges with a new GraphEdge.crossOrigins: string[] field carrying both sides' origins, captured at detection time before any later collision can reattribute either endpoint. scopeGlobalGraph now withholds the edge if EITHER entry is in the withheld set. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks — confirmed, this is real, and your example pinpoints exactly why round 12's fix was incomplete. Round 12 tagged a synthesized cross-repo edge with only the origin of whichever side Fixed by replacing the single-valued origin tag on a cross-repo edge with a new Added a new test reproducing your exact scenario (source-side collision) alongside the existing matched-side one, plus unit coverage in 161 tests passing (58 new/rewritten), typecheck and lint clean. Pushed as |
|
Findings
The other previously reported findings appear resolved, and the PR description includes sufficient representative real-CLI verification. |
…ttributable legacy cross-edges, and union crossOrigins on merge (Tencent#974 review round 14) Three gaps, two P1 and one P2: - detectCrossRepoEdges' reverse scan derived the importing side's origin by re-looking-up whichever node CURRENTLY sits at the import edge's `from` slug, instead of using that edge's own origin tag. If a withheld importer's node slug was already overwritten by a later, unrelated allowed repo before the target repo gets processed, the synthesized edge ends up tagged with two allowed origins and survives scoping. Fixed by preferring edge.origin (stable once tagged, immune to node collisions) over a fresh fromNode lookup, falling back to it only for edges that predate origin tagging. - The per-repo-file fallback (for codebases not covered by origin tagging) can never discover a cross-repo edge at all, since cross edges are synthesized straight into the global graph and never written to any per-repo file. When such a withheld codebase's node survives a slug collision with an allowed repo, an untagged legacy cross-edge touching that slug has no path left to removal. Fixed by failing closed (returning null) when a fallback-withheld codebase's contested slug is an endpoint of an edge that looks like a legacy, unattributable cross-repo edge (DEPENDS_ON, no origin/crossOrigins/source). - mergeGraphs silently discarded one side's crossOrigins whenever two codebases independently produced a cross-repo edge with the identical from/to/relation identity, making removal depend on merge order. Fixed by unioning crossOrigins on collision instead of overwriting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks — all three confirmed and fixed. import-repo.ts:145 (stale fromNode lookup). Confirmed exactly as described: the reverse scan re-derived the importing side's origin from whichever node currently sits at the import edge's graph-aggregate.ts:294 (unattributable legacy cross-edge). Confirmed — per-repo files never list a synthesized cross-repo edge (it's written straight to the global graph, never to any per-repo file), so the fallback mechanism used for a codebase origin tagging has never covered has no way to discover one at all. When such a codebase's node survives a legitimate slug collision with an allowed repo, an untagged legacy cross-edge touching that slug had no remaining removal path, and there's no way to retroactively recover its true provenance. Took your suggested direction: fail closed. graph-aggregate.ts:213 (shared edge-identity collision). Confirmed — Added regression tests for all three plus a direct-source probe reproducing each end to end (details in the PR body's Real-CLI Verification section). 168 tests passing, typecheck and lint clean. Pushed as |
|
Findings
The other previously reported findings appear resolved. The PR description includes sufficient representative real-CLI verification. |
…ot a flat origin set (Tencent#974 review round 15) Round 14 unioned a cross-repo edge's two origins into a flat crossOrigins set on merge collision, so withholding EITHER origin removed the edge — discarding a different, fully-allowed repo pair's equally valid claim on the identical edge identity. Replaced crossOrigins: string[] with crossOriginPairs: string[][], one pair per independent detection event; scopeGlobalGraph now withholds the edge only when EVERY pair has a withheld member, so an edge that remains independently producible by a fully-allowed pair survives. This also uncovered that buildAggregatedGraph appended newly-detected cross-edges to the global graph with a raw push, bypassing mergeGraphs entirely — so two independent detections sharing one from/to/relation key never actually got unioned onto one edge object; they sat as separate array entries both removable by the same shared key. Routed cross-edges through mergeGraphs instead, which is what makes the pair-tracking above actually take effect in the real aggregation pipeline, not just in a direct mergeGraphs unit test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Confirmed — this is real, and it's the exact tradeoff I flagged when I said "flagging that tradeoff explicitly in case you'd rather see it taken further." Taking it further now. Replaced Implementing this surfaced a second bug the round-14 unit test alone never would have caught: Added a |
|
Findings
The previously reported findings appear resolved, and the PR description includes sufficient representative real-CLI verification. |
…igin, and broaden the unattributable-edge fail-closed check (Tencent#974 review round 16) import-iwiki.ts's direct-match path writes a MAPS_TO edge straight to the global graph pointing at a bare fact-level node slug (e.g. component/App), with no origin tag at all. If the withheld codebase that supplied the matched node later collides with an allowed codebase on that same unqualified slug, collision restoration correctly keeps the node, but the untagged edge had no way to be identified as withheld — the evidence/code/<slug>/ prefix pass can't help (bare slugs carry no codebase prefix), and the edge never lived in any per-repo file either. Fixed at the source, consistent with the cross-repo-edge fixes: capture the matched node's own origin at match time and stamp it onto the edge, immune to any later collision reattributing the node. scopeGlobalGraph needed no changes for the common case — its existing single-origin tag pass already handles it. Also generalized the round-14 fail-closed guard (for a withheld, not-tag-covered codebase's contested slug touched by an unattributable edge) beyond DEPENDS_ON-relation edges: a legacy, untagged MAPS_TO edge from before this fix existed poses the identical risk, and nothing about the guard's reasoning was actually relation-specific. Also fixed a latent crash in reconcileIwikiWithCodebase: it read raw node.label/node.id (the legacy field names) with no fallback to the current title/slug schema, since it does a plain JSON.parse rather than going through loadGraphIndex's field normalization. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Confirmed — this is the exact same class of bug as the cross-repo edges (rounds 12-15), just via Fixed the same way: Also generalized the round-14 fail-closed guard (for a legacy, not-tag-covered codebase's contested slug touched by an edge we can't attribute) beyond Writing the regression test surfaced an unrelated, pre-existing latent crash: Added a new |
|
Findings
All previously reported findings appear resolved. The PR description includes sufficient representative real-CLI verification. |
…enance alongside crossOriginPairs (Tencent#974 review round 17) scopeGlobalGraph's edge-withholding check used `edge.crossOriginPairs ?? (edge.origin ? [[edge.origin]] : [])`, so when an edge carried BOTH fields the nullish fallback discarded `origin` entirely. mergeGraphs can produce exactly that shape: an allowed repo's own plain origin-tagged edge colliding (same from/to/relation) with a synthesized cross-repo edge. If every crossOriginPairs entry was withheld, the edge got removed even though its own, separately-allowed origin tag was an equally valid, independent derivation of it. Fixed by concatenating origin onto crossOriginPairs as one more length-1 pair instead of letting either field win outright — an edge is withheld only when every pair, origin included, has a withheld member. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Confirmed — a real gap in round 15's fix, exactly as described. The Added a direct |
|
Findings
All other previously reported findings appear resolved. The PR description includes sufficient representative real-CLI verification. |
…pair during mergeGraphs collisions (Tencent#974 review round 18) Round 17 fixed scopeGlobalGraph to treat an edge's origin and crossOriginPairs as independent alternatives when BOTH happened to be present on the same edge object. But mergeGraphs itself never actually produced that combined shape correctly: when a plain origin-tagged edge collided with a crossOriginPairs-only edge sharing the same from/to/relation identity, mergeGraphs only unioned crossOriginPairs — the losing side's plain origin was never read at all, so it vanished from the merged edge entirely rather than surviving as one more independent provenance pair. Fixed by treating each side's origin as its own one-element pair before unioning, symmetrically for both sides of the collision, not just whichever one happens to still be present on the winning object afterward. This also generalizes correctly to two plain edges colliding (previously left both untouched with only the tie-break winner's origin surviving) and is proven end-to-end through the real aggregation pipeline, not just a direct mergeGraphs call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Confirmed — round 17 fixed the consumer (scopeGlobalGraph) but not the producer (mergeGraphs), which never actually created the combined origin+crossOriginPairs shape correctly in the first place. When a plain This also generalizes correctly to two plain edges colliding (same gap, previously just not flagged) and I proved it end-to-end through the real aggregation pipeline — not just a direct |
|
Findings
The previously reported findings appear resolved. The PR description includes sufficient representative real-CLI verification. |
…global graph (Tencent#974 review round 19) A standalone `teamai codebase --extract` (outside `teamai import`'s cache-dir-then-copy orchestration) writes its graph straight into the real teamwiki's global `.indices/graph-index.json` — the only tagging step, buildAggregatedGraph, never runs for it. A withheld codebase re-extracted this way afterward produced untagged nodes/edges with no path to origin-based removal, while its evidence/code/<slug>/ per-repo file (from an earlier `teamai import`) sat stale and unreadable as a reliable fallback either. Fixed at the source, consistent with every other origin-tagging fix in this PR: stamp origin = project on every node and edge extractCodebase writes, immediately before saveGraphIndex, regardless of whether this call is a standalone extract (writes straight to the real global file) or part of import's orchestration (writes to a cache location later copied into the per-repo file and re-tagged identically by aggregateGlobalGraph — redundant but harmless there). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Confirmed — a real, previously-unaddressed gap. Traced it down to confirm the exact mechanics: a standalone Fixed at the source, same as every other origin-tagging gap in this PR: Added a new test block in |
|
No findings. The previously reported direct |
Problem
Docs and learnings are already scoped by role/project namespace (#707), but
teamwikiwas not. A checkout active on one project (e.g.--project svc-a) gotteamai recallhits from every codebase'sevidence/code/<slug>/, including slugs that belong to an unrelated project — recall would surface another project's codebase evidence just because it lived in the same team wiki.Fix
Adds a hand-declared
wikiresource type inmanifest-schema.ts, following the exact patterndocsalready uses:HAND_DECLARED_RESOURCE_TYPESgains'wiki'— this alone threads it through the existing generic plumbing inroles.ts/projects.ts(NAMESPACED_RESOURCE_TYPES,mergeNamespaces,resolveRoleResourceNamespaces,resolveProjectResourceNamespacesalready loop overHAND_DECLARED_RESOURCE_TYPESgenerically), with no changes needed there.resource-namespaces.tscomputesinactiveWikiNamespacesthe same way it already computesinactiveDocsNamespaces: a slug ANY role or project declares underresources.wikiis withheld unless this directory's active role/project selects it; an undeclared slug stays shared (no change for existing teams).code-knowledge-recall.ts'sloadWikiPages/queryCodeKnowledgetake a newwithheldCodebasesoption and filterevidence/code/<slug>/directories by it, case-folded.recall.tswires the two together: computeswithheldCodebasesfrom the active project config viaresolveResourceNamespacesand passes it toqueryCodeKnowledge.The slug is whatever
teamai codebase --project <slug>wrote — unrelated to a manifest project id — so a team declares the one it already uses:Review follow-up round 1: route depth and the knowledge graph also needed scoping
codex-reviewfound the page-directory filtering above left two more surfaces unscoped:--depth routereturned the globalteamwiki/router.mdverbatim, which lists every codebase's name/link/description/keywords plus a<!-- search-anchor -->keyword comment. Fixed by stripping withheld-codebase lines and the anchor comment from the content before returning it..indices/graph-index.jsonwas loaded unfiltered atcontext/lookupdepth, so a withheld codebase's nodes could match as BM25 entry nodes, boost scores via graph neighbors, or surface a withheld codebase's own node id through an allowed page'srelatedFilesvia a cross-repoDEPENDS_ONedge.Also fixed the two propagation gaps the same review round flagged: the "upgrade every member first" warning and the directory-naming-rule list in
manage-admin.mdnow includewikialongsideenv/hooks/mcp/models/docs, and the--namespacesCLI help text (+ generatedcommands.md) anddocs/designs/multi-project-management.md's resource-type enumeration now mentionwikitoo.Review follow-up round 2: the round-1 router fix missed a link format, and the round-1 graph fix dropped legitimate global-only edges
[[evidence/code/<slug>/index]]links (routerTemplate's format).rebuildWikiIndex()— the path actually taken after an import — writes[[code/<slug>/index]]table rows instead, with noevidence/prefix, so a codebase imported that way still leaked its full row. Fixed by widening the line-match regex to accept an optionalevidence/prefix beforecode/<slug>..indices/graph-index.json— chieflyteamai codebase --reconcile's product↔codeMAPS_TOedges, since the reconciler reads and writes the global graph directly and never touches a per-repo file. Activating any wiki restriction therefore silently dropped those mappings even for codebases that were still fully allowed. Fixed by flipping the approach:scopeGlobalGraph(replacingbuildAggregatedGraph'sexcludeProjects) now starts from the real global graph and subtracts exactly the withheld codebase's own node slugs (read from its per-repo file, which still correctly names every fact-level node it contributed, prefixed or not), then drops any edge left dangling from a removed endpoint. That dangling-edge cleanup is what still removes a cross-repoDEPENDS_ONedge into a withheld node — the original leak this graph-scoping effort started from — without needing to know the edge came from cross-repo detection rather than a per-repo file.resolveResourceNamespaces()inrecall.tsran outside the existing code-retrievaltry/catch, so an unreadable or malformed manifest rejected the whole recall call even when a valid learnings index had already been searched safely. Moved it inside the sametry, so it now degrades the same way aqueryCodeKnowledgefailure already does: skip code recall, warn, keep the rest of the results.Review follow-up round 3:
scopeGlobalGraphitself (round 2's fix) over-pruned, plus a real but smaller slug-collision ambiguityscopeGlobalGraph's edge filter required both endpoints to survive as nodes (keptSlugs.has(e.from) && keptSlugs.has(e.to)). But an AST/heuristic edge is commonly file-to-file ({from: 'src/a.ts', to: 'src/b.ts'}) with neither endpoint present innodes[]at all — so the moment anything was withheld, every such edge vanished for every codebase, allowed ones included, dropping their files fromrelatedFiles/"Candidate change files". Fixed by subtracting by identity instead: collect every node slug and edge endpoint a withheld codebase's own per-repo file contributed, then drop only edges actually touching one of those — not edges that merely fail to resolve to a known node.buildCodeGraphmintscomponent/Appthe same way for any repo), so an allowed and a withheld repo can legitimately collide on one slug after merging; subtracting by slug alone would also remove the allowed repo's node. Fixed by also collecting every allowed codebase's own identifiers and clearing any that overlap with the withheld set before subtracting — a shared identifier is never removed.docs/usage-guide.md(+ zh-CN) still said--namespaces"neither touches env, hooks, mcp, models or docs", omittingwiki, andCHANGELOG.md's feat(projects): extend project isolation to knowledge/docs/agents/env/hooks/mcp #707 entry enumeratingresources:keys was stale the same way. Addedwikito all three.Review follow-up round 4: router filtering missed the AI-domain branch's unlinked lines, and
scopeGlobalGraphmissed reconcile-added code-page nodescode/<slug>link.routerTemplate()'s AI-domain branch, though, emits a### <domain>header line with no link at all, and an unresolved component (one whose name does not match any known project) as a bare- <name>line with no link either. Once a withheld codebase's properly-linked sibling lines were removed, an all-withheld domain's header — and any such unlinked fallback line under it — survived untouched, still naming the domain and component. Fixed by groupingrouter.mdinto sections at each markdown header first: a section whose links are all withheld (and at least one was found) is now dropped whole, header and any unlinked lines included; a section mixing allowed and withheld links still keeps its header and drops only the withheld lines, as before.scopeGlobalGraphidentified withheld content solely from per-repo graph files, butteamai codebase --reconcileadds code-page nodes (evidence/code/<slug>/<page>) and theirMAPS_TOedges straight to the global graph, the same way it adds product-page nodes — never to a per-repo file. After reconciling and then withholding that codebase, those nodes/edges survived, so a query matching the withheld page's title could still use it as an entry node or graph boost. Fixed with a second pass over the global graph's own nodes, matched by theevidence/code/<slug>/prefix (which, unlike a bare fact-level slug, unambiguously names the codebase that owns it, so there is no allowed/withheld collision risk the way there is for prefix-less slugs).Review follow-up round 5:
scopeGlobalGraphstill failed open when a withheld codebase has no per-repo graph file at all, plus a narrower colliding-edge gapteamai codebase --extractrun directly (not throughteamai import's cache-then-copy orchestration) writes only the globalteamwiki/.indices/graph-index.jsonand never populatesevidence/code/<slug>/.indices/graph-index.jsonfor that codebase.scopeGlobalGraph's withheld-identifier collection only reads that per-repo path, so a codebase extracted this way had no identifiers collected for it at all when withheld — its fact-level nodes and edges stayed in the "scoped" graph fully exposed. Fixed by failing closed, not open: if any withheld codebase's per-repo graph file is missing or unreadable,scopeGlobalGraphnow returnsnull(no graph at all for this query) rather than a result it cannot vouch for.queryCodeKnowledgealready treats anullgraph as "skip graph-based features," the same as when no graph exists at all, so no consumer-side change was needed.component/App) correctly keeps both repos' nodes, but a withheld repo can also have an edge directly between two such colliding names — a relationship that only ever existed in the withheld repo, which neither endpoint-identifier removal nor the "shared identifier survives" rule catches, since both endpoints end up allowed. Fixed by tracking edge pairs (from|to) the same way identifiers are tracked: a withheld-only pair is subtracted even when both of its endpoints individually survive; a pair also present in an allowed repo's own graph is still preserved.Review follow-up round 6: a mixed router section still leaked an unattributable line, a colliding node's metadata could still be the withheld repo's, and two smaller gaps
routerTemplate's "no project match" branch, no link at all) survived regardless, sincelineSlug()can never attribute an unlinked line to either side.filterRouterContentonly ever runs when something IS withheld, so that ambiguity can't be resolved safely either way — fixed by dropping every unlinked bullet line unconditionally, the same fail-closed-on-ambiguity rulescopeGlobalGraphalready applies to graph ownership it cannot verify.mergeGraphslets the later-processed repo's node win outright with no field-level merge, so the surviving global node could still carry the withheld repo's title/domain if that repo's write happened to win — querying that title could then select it as an entry node and affect scoring. Fixed by re-attaching the allowed repo's own copy of a contested node (read in the same per-repo scan already in place). This also surfaced a second bug while writing the test for it: the existing "nothing to remove" fast path returned the graph untouched before this restoration ever ran, for the exact case where the only withheld content is a slug that's also allowed (nothing to remove, but still something to correct) — fixed by tracking contested slugs before the allowed set clears them out of the withheld set, and including that count in the fast-path's guard.relation, so an allowedApp -REFERENCES-> Configcleared a withheldApp -DEPENDS_ON-> Configout of the withheld set even though it never claimed that specific relation — both edges then survived. Fixed by keying edges onfrom|to|relation, matching the graph schema's own edge identity.usage-guide.md(+ zh-CN) said wiki scoping needs a team repo "withmanifest/projects.yaml", but a role-only declaration inmanifest/roles.yamlworks with noprojects.yamlat all. Reworded to not implyprojects.yamlis required.Review follow-up round 7: two more real gaps inside
scopeGlobalGraphitself, plus an edge-identity nitJSON.parsesucceeded, with no check that the result was actually a graph. A truncated or otherwise corrupted write can leave syntactically valid JSON ({}) that is not a graph at all —nodes ?? []/edges ?? []would silently contribute zero identifiers while the global graph still has that codebase's real content, so scoping would run as if there were nothing to subtract. Fixed by validating the parsed result hasnodes/edgesarrays before marking it accounted for; anything else now falls into the same fail-closed path as an unreadable file.loadGraphIndexnormalizes the legacyimportsrelation name toDEPENDS_ONwhen it loads the global graph.scopeGlobalGraphreads per-repo files with a plainJSON.parse, bypassing that normalization — so a withheld repo's ownimportsedge computed a key that could never match the already-normalizedDEPENDS_ONedge in the loaded global graph, letting that withheld relationship survive. Fixed by normalizing the relation the same way before computing the key (exportedLEGACY_RELATIONSfromgraph-index.schema.tsrather than duplicating the mapping).from|to|relationwith a plain|, which a|-containing identifier could alias (a|b -> cvsa -> b|c). Switched toJSON.stringify([from, to, relation]), matchinggraphEdgeKey's own approach ingraph-index.schema.ts.Review follow-up round 8: the round-7 validation was itself too shallow, and it explains a second metadata-restoration gap
nodes/edgeswere arrays. A node that doesn't conform toGraphNodeSchema(missing required fields, wrong types) still passed that check and was silently skipped (no slug extracted) while the whole file still counted as "successfully read ownership from" — so a withheld codebase with any schema-invalid nodes mixed into real ones could leave those real nodes exposed. Fixed by reusingparseGraphIndex(now exported fromgraph-index.schema.ts) — the exact schemaloadGraphIndexvalidates the global graph with — instead of a loose array check.id/label/kindshape, the raw object has notitlekey at all (onlylabel) — so the spread never actually overrodetitle, leaving the withheld repo's title in place if its write had won the merge. Fixed by the sameparseGraphIndexreuse: it normalizeslabel→title,id→slug,kind→typethe same way the global graph itself was normalized on load, so the node now stored for restoration carries the right field names. (This reuse also let the hand-rolled relation-normalization helper added last round come back out —parseGraphIndex's edge schema already does it.)Review follow-up round 9: a linkless all-withheld domain still leaked its header, and a schema-valid-but-empty withheld graph file was trusted too readily
routerTemplate's project match) has nocode/<slug>links at all. The section-level "all-withheld" check requires finding at least one withheld link to decide a section is fully withheld, so an emptyslugsarray never triggered it — the bare bullets still got dropped individually, but the### <domain>header above them had nothing left to filter it by and survived, still naming the domain. Fixed by deciding at the bullet level instead of the slug level: if a section had any bullets at all and NONE of them survive line filtering — withheld-linked or unattributably bare — the whole section (header included) is now dropped.scopeGlobalGraphalready failed closed on a withheld codebase's per-repo file that's missing, unreadable, or schema-invalid — but a schema-valid, entirely empty one ({nodes: [], edges: []}) still parsed cleanly and was marked accounted for while contributing nothing to subtract. If that file goes stale or is truncated after an earlier successful aggregation, the global graph can still hold the withheld codebase's real nodes with nothing left to remove them. A genuine extraction's per-repo graph is never actually empty (buildIndexHubOverlayunconditionally adds the project's own index/hub node whenever extraction produces any page), so an empty graph for a withheld codebase is now distrusted the same as an invalid one.Review follow-up round 10: every per-repo-file fail-closed guard shared one root assumption that an interrupted import can break
[P1] Every guard added over the last several rounds (missing, unreadable, schema-invalid, empty) still assumed a withheld codebase's current per-repo file completely and accurately describes everything it ever contributed to the global graph. An interrupted
teamai importbreaks exactly that: it can replace a withheld codebase's per-repo graph with a different, valid, non-empty one and be interrupted before the next aggregation folds the change in, leaving the global graph holding OLD nodes/edges that no longer appear anywhere in the per-repo filescopeGlobalGraphwould read. No amount of validating that file harder closes this — it can be perfectly valid and simply describe something else now.Fixed at the root, per the review's own suggestion: ownership is now stamped onto the data at the moment it is merged, not re-inferred from a separate, independently-mutable file at query time.
buildAggregatedGraphtags every node and edge from a per-repo file withorigin: <that codebase's slug>before merging it in (originis now a first-class optional field onGraphNode/GraphEdge). That tag travels with the data in the global graph forever after, correct regardless of whatever the per-repo file is later rewritten to, emptied to, or deleted to.scopeGlobalGraphnow checks tags first: a withheld codebase with ANYorigin-tagged content in the global graph is "tag-covered," and its removal is read directly off those tags — no per-repo file involved at all. Only a codebase tagging has never covered (predates this field, or was extracted directly viateamai codebase --extractoutsideteamai import's per-repo-file-producing path) falls back to the existing per-repo-file mechanism, with all its existing fail-closed guards intact for exactly that narrower, legacy case. Allowed codebases are still always read regardless of tagging, since collision handling (two codebases sharing one unqualified fact-level slug) needs an allowed codebase's own data to restore — the merged graph keeps only one node per colliding slug, so tagging alone can't tell "this slug is ALSO legitimately an allowed codebase's."Review follow-up round 11: a reconcile-materialized endpoint node had no origin of its own, plus a docs gap
teamai codebase --reconcilerepairs acode-ast/code-heuristicedge whose endpoint isn't a node yet by minting a placeholder node for it (loadReconciliationBase'sendpointNodesloop). That placeholder never copied the edge's ownorigintag, so round 10's tag-based removal — which skips the per-repo-file fallback entirely once a withheld codebase has ANY tagged content ("tag-covered") — correctly removed the withheld codebase's tagged edge but left this origin-less placeholder node behind and searchable, letting withheld content affect entry-node matching and graph boosts. Both endpoints of an intra-repo AST/heuristic edge belong to the same codebase that produced the edge, so the fix stampsorigin: edge.originon the node at the moment it is minted — closing the gap at creation time, consistent with round 10's "tag ownership once, at the source" approach, rather than adding another special case toscopeGlobalGraphitself (which needed no changes: its existing origin-tag pass already removes a tagged node).product-overview.md's resource-namespace summary (and its zh-CN counterpart) documented that skills/rules/docs/etc. can live under a<namespace>/subdirectory, but never mentioned that the Team Context codebase knowledge graph below it is scoped the same way viaresources.wiki(already documented in depth inusage-guide.md, just missing a pointer from the higher-level overview). Added a one-line cross-reference in both.Review follow-up round 12: a cross-repo edge could outlive a slug collision, and an originless edge endpoint slipped past the reconcile-added-content pass
detectCrossRepoEdgessynthesizes an edge spanning two codebases (by label match) and left it fully untagged — correct in isolation, since no single origin fully describes a two-codebase edge. But if the matched node's unqualified slug was later also minted by a THIRD, allowed repo and that repo's write won the merge (collision handling's "allowed repo's node survives" rule, working as designed), the edge then had no way to be tagged withheld at all: the node it pointed at no longer carried the withheld repo's origin, and the edge itself never did either. Fixed by tagging the edge, at the exact momentdetectCrossRepoEdgesmatches it, with the origin of whichever side was found via label lookup in the OTHER graph — the specific codebase this edge's existence actually depends on, captured before any later collision can silently reattribute it.scopeGlobalGraphneeded no changes: its edge-level origin-tag pass already removes any edge once it carries a withheld tag.evidence/code/<slug>/prefix pass that catches--reconcile-added content (nodes/edges written straight to the global graph, never to a per-repo file) only ever scannednodes[].import-iwiki.tshas its own, separate direct-to-global-graph writer that can add aMAPS_TOedge pointing at a code page path (evidence/code/<slug>/<page>.md) purely because a doc term appears somewhere in that page's text — with no node ever created for the page itself. That edge's endpoint was therefore never innodes[]to be matched by the prefix check, so it survived scoping untouched. Fixed by running the identical prefix check over edge endpoints too.Review follow-up round 13: a cross-repo edge needs BOTH its origins tracked, not just the matched side
detectCrossRepoEdgesmatched by label lookup. But the relationship depends on BOTH sides — the repo whose own import produced the match, AND the repo that was matched. The review's own example: withheldsvc-bimports allowedsvc-a, while allowedsvc-chappens to sharesvc-b's unqualified SOURCE-node slug (not the matched/target side round 12 tested). Collision handling correctly letssvc-c's node survive, but the edge's lone tag was the matched side's origin (svc-a, allowed) — so withholdingsvc-bleft the edge with no path to removal at all, even though it only exists because ofsvc-b's own import. Fixed by replacing the single-valued tag on a cross-repo edge with a newGraphEdge.crossOrigins: string[]field carrying BOTH sides' origins, captured at the moment of detection — before either endpoint can be silently reattributed by a later collision.scopeGlobalGraphwithholds the edge if EITHER entry is in the withheld set.Review follow-up round 14: the importing side's origin could itself be stale, a legacy cross-edge had no removal path at all, and a merge collision could silently drop provenance
detectCrossRepoEdges's reverse scan (existing.edgesmatched against the new repo) derived the importing side's origin by re-looking-up whichever node CURRENTLY sits at the import edge'sfromslug inexisting.nodes— butexistingaccumulates every repo processed so far, so if a withheld importer's node slug was already overwritten by a LATER, unrelated allowed repo before the repo it imports gets processed, that lookup returns the wrong (allowed) origin. The synthesized edge then ends up tagged with two allowed origins and survives scoping entirely, even though it only exists because of the withheld repo's own import. Fixed by preferring the import edge's OWN origin tag (edge.origin, stamped once and immune to any later node collision) over a fresh node lookup, in both the forward and reverse scans; falling back to the node lookup only for edges that themselves predate origin tagging.origin/crossOriginstagging entirely and touches that slug has no remaining path to removal. Retroactively recovering that edge's true provenance isn't possible — the information needed to attribute it was never persisted. Fixed by failing closed instead: if a fallback-withheld codebase's contested slug is the endpoint of an edge that looks like a legacy, unattributable cross-repo edge (DEPENDS_ONrelation, noorigin, nocrossOrigins, nosource),scopeGlobalGraphnow returnsnullrather than risk exposing it. Scoped precisely to fallback-sourced contested slugs so it cannot fire for the common, fully tag-covered case.mergeGraphsdeduplicates edges byfrom|to|relationand let whichever side won the tie-break silently discard the OTHER side'scrossOriginsoutright. If two DIFFERENT codebases independently produce a cross-repo edge with the identical identity (e.g. both import something matching the same third repo's component under an equally generic importer slug of their own), the edge's final origin set — and therefore whether withholding one of them removes it — depended on which repo happened to merge last. Fixed by unioningcrossOriginson collision instead of overwriting, making removal correctly trigger regardless of merge order (though a single edge-key still can't distinguish two fully-independent, unmerged provenances enough to let one survive while only the other is withheld — an edge touched by any withheld origin is still removed, erring toward the safe/fail-closed direction for this narrow, rare case).Review follow-up round 15: a flat origin set on a cross-repo edge discarded a different, fully-allowed repo pair's equally valid claim on it
[P2] Round 14's
crossOrigins: string[]fix unioned both sides' origins on a merge collision, but flattened them: if withheldsvc-band allowedsvc-cBOTH independently produce the identical cross-repo edge (e.g. both import something matching the samesvc-acomponent under an equally generic importer slug of their own), the union became[svc-b, svc-c, svc-a]— withholdingsvc-balone removed the edge, discardingsvc-c's fully-allowed, entirely independent relationship to the exact same edge identity. ReplacedcrossOrigins: string[]withcrossOriginPairs: string[][]— one pair per independent detection — andscopeGlobalGraphnow withholds the edge only when EVERY pair has a withheld member, so an edge independently producible by a fully-allowed pair survives.Digging into this surfaced a second, deeper bug the round-14 unit test alone couldn't catch:
buildAggregatedGraphappended newly-detected cross-edges to the global graph with a raw.push(), bypassingmergeGraphsentirely. So two independent detections sharing onefrom/to/relationkey never actually got merged onto one edge object in the real aggregation pipeline — they sat as two separate array entries, both removable by the same shared key the moment either one was tagged withheld. Routed cross-edges throughmergeGraphsinstead, which is what makes the pair-tracking fix above actually take effect end to end, not just in amergeGraphscall made directly.Review follow-up round 16: import-iwiki.ts's own direct-match MAPS_TO edges had the identical untagged-edge gap as the cross-repo edges
[P1]
import-iwiki.ts's direct-match path (a doc term matching a code node's label exactly) writes aMAPS_TOedge straight to the global graph pointing at a bare fact-level node slug (e.g.component/App), with no origin tag at all. If the withheld codebase that supplied the matched node later collides with an allowed codebase sharing that same unqualified slug, collision restoration correctly keeps the node — but the untagged edge had no path to removal: theevidence/code/<slug>/prefix pass can't help (a bare fact-level slug carries no codebase prefix to match), and the edge never lived in any per-repo file either. Fixed the same way as the cross-repo edges: capture the matched node's own origin at match time and stamp it onto the edge, immune to any later collision reattributing the node.scopeGlobalGraphneeded no changes for this common case — its existing single-origin tag pass already handles any edge once it carries one.Also generalized the round-14 fail-closed guard (for a withheld, not-tag-covered codebase's contested slug touched by an edge with no way to verify ownership) beyond
DEPENDS_ON-relation edges specifically — a legacy, untaggedMAPS_TOedge predating this fix poses the identical risk, and nothing about the guard's reasoning was ever actually relation-specific.Writing a test for this surfaced a pre-existing, unrelated latent crash:
reconcileIwikiWithCodebaseread rawnode.label/node.id(the legacy field names) with no fallback to the currenttitle/slugschema, since it parses the global graph file directly rather than throughloadGraphIndex's field normalization — so it would throw against any graph written in the modern schema. Fixed alongside, since the new regression test couldn't otherwise exercise the fix against realistic data.Review follow-up round 17: an edge's plain
originand itscrossOriginPairsare not mutually exclusive, but the removal check treated them as if they werescopeGlobalGraph's edge-withholding check readedge.crossOriginPairs ?? (edge.origin ? [[edge.origin]] : [])— ifcrossOriginPairswas present at all,originwas ignored outright, even when BOTH were set on the same edge.mergeGraphscan produce exactly that shape: an allowed repo's own plain,origin-tagged edge colliding (identicalfrom/to/relation) with a separately-detected synthesized cross-repo edge. If everycrossOriginPairsentry ended up withheld, the edge was removed even though its own, independently-allowedorigintag was an equally valid derivation of it that had nothing to do with the cross-repo pairs. Fixed by concatenatingoriginontocrossOriginPairsas one more length-1 "pair" instead of letting either field win outright — the edge is withheld only when EVERY pair,originincluded, has a withheld member.Review follow-up round 18: mergeGraphs itself, not just scopeGlobalGraph, needed to preserve a losing edge's plain origin
scopeGlobalGraphto treat an edge'soriginandcrossOriginPairsas independent alternatives once BOTH happened to be present on the same edge object — butmergeGraphsitself never actually produced that combined shape correctly in the first place. When a plainorigin-tagged edge collided (identicalfrom/to/relation) with acrossOriginPairs-only edge,mergeGraphsonly ever unionedcrossOriginPairs; the LOSING side's plainoriginwas never read at all, so it vanished from the merged edge entirely rather than surviving as one more independent provenance pair. Fixed by treating each side'soriginas its own one-element pair before unioning, symmetrically for both sides of the collision — not just reading whatever field happens to still be present on the object that wins the evidence tie-break. This also generalizes correctly to two plain edges colliding (previously the losing side's origin was dropped outright) and is proven end-to-end through the real aggregation pipeline, not only a directmergeGraphscall.Review follow-up round 19: a direct
teamai codebase --extractnever got origin-tagged at all, not even via the fallbackteamai codebase --extract(outsideteamai import's cache-dir-then-copy orchestration) writes its graph straight into the real teamwiki's global.indices/graph-index.json—buildAggregatedGraph, the only step that stampsorigin, never runs for it. Re-extracting a withheld codebase this way produced untagged nodes/edges with no path to origin-based removal, while itsevidence/code/<slug>/per-repo file — left over from an earlierteamai import— sat stale, describing older content, and couldn't be trusted as the fallback either. Fixed at the source, the same way as every other origin-tagging fix in this PR:extractCodebasenow stampsorigin = projecton every node and edge it writes, immediately beforesaveGraphIndex— whether this call is a standalone extract (writes straight to the real global file, now correctly tag-covered) or part ofimport's orchestration (writes to a cache location later copied into the per-repo file and re-tagged identically byaggregateGlobalGraph— redundant but harmless there).Docs
usage-guide.md(+ zh-CN),manage-admin.mdanddocs/designs/multi-project-management.mdeach get thewikinamespace documented alongside the existingdocsnamespace coverage.product-overview.md(+ zh-CN) now cross-references that coverage from its resource-namespace summary.Test plan
81 new/rewritten tests across 8 files:
resource-namespaces-wiki.test.ts—inactiveWikiNamespacesresolution against realroles.yaml/projects.yamlfixtures.code-knowledge-recall-wiki-scope.test.ts—withheldCodebasesfiltering against realteamwikifixtures: case-fold matching, route-depth filtering for both router link formats (evidence/code/<slug>andcode/<slug>), the AI-domain-grouped format (an all-withheld domain section dropped whole vs. a mixed one keeping its header and dropping an unattributable bare line too, and a domain whose every component is an unmatched bare line dropped whole despite carrying no links at all), cross-codebaserelatedFilesleakage via a graph edge, and preservation of a global-only forward edge (the kind--reconcileadds directly to the global graph) when the withheld codebase is unrelated to it.recall-wiki-scope.test.ts—recall()'s wiring (the exactwithheldCodebasesargumentqueryCodeKnowledgereceives) plus a regression test that an unreadableprojects.yamldegrades to skipping code recall instead of rejecting the whole call.graph-aggregate.test.ts—scopeGlobalGraph: subtracts a withheld project'sorigin-tagged nodes and edges directly from the global graph (the primary mechanism now), removes a cross-repo edge into a withheld node, removes a--reconcile-added withheld code-page node and itsMAPS_TOedge even though neither ever lived in a per-repo file, subtracts a withheld-only edge between two colliding slugs by its exact relation (including the legacyimportsname) while keeping an allowed edge under a different relation, restores an allowed repo's own title/domain for a colliding slug even when the withheld repo's version won the merge (including the legacylabelfield), removes a withheld codebase's tagged content correctly even after its per-repo file goes structurally wrong, stale-empty, or is replaced by a newer non-overlapping graph before re-aggregation runs (the review's exact interrupted-reimport scenario), falls back to the per-repo file (with the same fail-closed guards as before) only for a codebase tagging has never covered, and still fails closed for that narrower case when the file is missing or schema-invalid, does not fail closed when only an unrelated allowed codebase lacks one, keeps a global-only edge/node unrelated to the withheld project, keeps an allowed codebase's file-to-file edge whose endpoints are not declared nodes at all, keeps an allowed codebase's node when a withheld codebase happens to mint the identical unqualified slug, case-fold matching, and the no-op/empty-input cases.codebase-reconcile.test.ts— the round-11 fix: a reconcile-repaired endpoint node for acode-astedge is tagged with the sameoriginas that edge, andscopeGlobalGraphthen correctly removes both the originally-missing endpoint nodes when that codebase is withheld.cross-repo-edges.test.ts/graph-aggregate.test.ts— the round-12/13 fixes:detectCrossRepoEdgestags its synthesized edge withcrossOriginscarrying BOTH sides' origins (omitting a side that predates origin tagging), and two full aggregate-then-scope tests reproduce both collision directions end to end — a cross-repo edge survives a later, allowed repo winning a same unqualified slug on either its matched (target) side or its source (importer) side right up until these fixes, and is gone after them, while the unrelated allowed repo's own edge and the now-allowed node(s) both still survive. A third test covers animport-iwiki.ts-style originlessMAPS_TOedge intoevidence/code/<withheld>/<page>.md.cross-repo-edges.test.ts— the round-14 importing-side fix: the reverse scan uses the import edge's own origin tag even when the node currently at itsfromslug has since been reattributed to an unrelated repo by a later collision, and falls back to that node's origin only when the edge itself predates tagging.graph-aggregate.test.ts— a hand-written, pre-upgrade-style fixture (global graph with a legacy, fully untagged cross-repo edge; two per-repo files colliding on its source slug, one withheld, one allowed) confirmsscopeGlobalGraphnow fails closed instead of silently keeping that edge. A newmergeGraphsdescribe block confirmscrossOriginPairsare unioned as SEPARATE pairs (not flattened/overwritten) on an edge-identity collision, order-independently, dedupes an identical pair contributed by both sides, and confirms a plain single-origin edge collision is unaffected.graph-aggregate.test.ts— the round-15 fix end to end through the real aggregation pipeline (not just a directmergeGraphscall): withheldsvc-band allowedsvc-cboth independently import something matching allowedsvc-a's component under the identical importer slug; the resulting single, merged edge carries both pairs, and withholding onlysvc-bkeeps the edge (svc-c's pair is fully allowed) while withholding bothsvc-bandsvc-cremoves it.import-iwiki-reconcile.test.ts(new) —reconcileIwikiWithCodebase's direct-match edges carry the matched node's own origin; a full aggregate → reconcile → simulate-later-collision →scopeGlobalGraphsequence confirms the edge is removed once that codebase is withheld even though its matched node survives the collision.graph-aggregate.test.ts— the broadened fail-closed guard: a hand-written legacy fixture with a fully untaggedMAPS_TOedge (instead ofDEPENDS_ON) into a contested fact-level slug still correctly fails closed.graph-aggregate.test.ts— the round-17 fix: a hand-written edge carrying BOTHoriginandcrossOriginPairssurvives when only thecrossOriginPairsside is withheld (theoriginside is independently allowed), and is removed once every side —originincluded — is withheld.graph-aggregate.test.ts— the round-18mergeGraphsfix: two plain, single-origin edges colliding now preserve both origins as independent pairs instead of the loser's vanishing; a plain edge colliding with acrossOriginPairs-only one preserves the plain side's origin regardless of merge order; and a full real-pipeline test (svc-a's own plain edge happens to collide with a cross-repo edge synthesized from withheldsvc-wimporting allowedsvc-z) confirmssvc-a's independent claim keeps the edge alive after withholdingsvc-w.codebase-extract-fallback.test.ts(new describe block) — the round-19 fix: every node/edge a directextractCodebasewrites to the real teamwiki's global graph carriesoriginset to the project slug, andscopeGlobalGraphcorrectly withholds all of it even with a deliberately stale, untouchedevidence/code/<project>/.indices/graph-index.jsonper-repo file sitting alongside it.All pass. Plus the 5 existing test files most directly touched by this change (
recall.test.ts,recall-scope-isolation.test.ts,pull-docs-namespaces.test.ts,resource-namespaces-case-alias.test.ts,projects.test.ts),import-repo-incremental.test.ts/import-repo-merge.test.ts/import-dir.test.ts(exerciseextractCodebase's real callers), andcommands-reference.test.ts(regenerated generated-docs snapshot) — 184 tests total — all still pass with zero regressions. (One pre-existing, unrelated Windows-only failure incodebase-reconcile.test.ts— a fixture path containing a literal|character, which Windows' filesystem rejects — reproduces identically onmainvia git-stash before/after and is untouched by this PR.) Typecheck and lint (oxlint --type-aware) both clean.Real-CLI verification (not just unit tests)
Built (
npm run build) and ran the actual CLI against a hand-built team repo + project fixture, across every review round:recall "octopus" --depth lookupwith onlysvc-aactive → only svc-a's page returned; with bothsvc-a/svc-bactive → both returned.recall "router" --depth route, across all three router formats (routerTemplate's plain bullets,rebuildWikiIndex's table rows, androuterTemplate's AI-domain-grouped sections, mixed and all-withheld) → in every case svc-b's content (including an all-withheld domain's header, an unlinked fallback line with no match inprojects[], and an unmatched bare line in an otherwise-allowed mixed domain) is stripped when onlysvc-ais active, while an allowed/mixed domain's header and line survive.a/client → b/service) surfacesb/serviceunder "Candidate change files" when both projects are active, and disappears whensvc-bis withheld — and a separate global-only forward edge (a/client → docs/product/billing, simulating what--reconcilewrites directly to the global graph) survives scoping and still appears, confirming the fix removes only the withheld codebase's own content.src/a.ts → src/b.ts, the real shape of an AST/heuristic edge) survives in "Candidate change files" even when an unrelated codebase (svc-b) is withheld — confirming the round-3 over-pruning fix.scopeGlobalGraphcall against a global graph with an injected reconcile-style withheld code-page node (evidence/code/svc-b/overview) andMAPS_TOedge confirms both are removed while an unrelated product-page node survives.projects.yamlnow makesrecallprint the existing "code graph retrieval unavailable" warning and exit 0, instead of crashing the whole command.--extract, never tag-covered) makes an unrelated, fully-accounted-for codebase's own "Candidate change files" entry disappear too the moment the no-graph-file codebase is withheld — confirming the per-repo-file fallback's fail-closed path still applies for a codebase tagging has never covered.scopeGlobalGraphcall against a graph where a withheld repo's write won the merge for a colliding slug now returns the allowed repo's title/domain for that slug, and keeps only the allowedREFERENCESedge between the colliding endpoints while dropping the withheldDEPENDS_ONone.importsrelation name is correctly normalized and removed, leaving only the allowedREFERENCESedge; and a per-repo file with a node that doesn't validate against the graph schema, for a codebase never successfully aggregated before, still makesscopeGlobalGraphreturnnull.code/<slug>link anywhere in the section) is now fully stripped from--depth route's snippet while an unrelated, properly-linked domain survives.scopeGlobalGraphcall reproducing the round-10 review's exact scenario — aggregate once (tagging a withheld codebase's node), then replace that codebase's per-repo file with a newer, valid, non-overlapping graph with no re-aggregation — confirms the OLD tagged node is still correctly removed, without the function ever reading (or being misled by) the replaced file. The same holds when that per-repo file instead goes structurally wrong or stale-empty post-aggregation: the whole query now stays usable, scoped precisely by the tag, instead of failing closed to no graph at all.teamai codebase --reconcilerun (the real CLI command, not a direct function call) against a per-repo graph with acode-astedge (src/a.ts → src/b.ts) whose endpoints are both missing fromnodes[]: the reconciler mints both placeholder nodes and both now carryorigin: "auth"(the review's exact scenario). A follow-upscopeGlobalGraph(..., new Set(['auth']))call confirms neither placeholder survives, while an unrelated product page does.svc-aimports something labeledBalanceService;svc-bdefines it;svc-c, processed aftersvc-b, mints the identical unqualified slug): confirmed the synthesized cross-repo edge carriesorigin: "svc-b"and the colliding node ends uporigin: "svc-c"after merge, then confirmedscopeGlobalGraph(..., new Set(['svc-b']))keeps the now-allowed node AND svc-a's own unrelated edge, but drops the cross-repo edge specifically. A second run injected animport-iwiki.ts-shaped originlessMAPS_TOedge directly into the global graph and confirmed it's gone after scoping too.svc-bimports allowedsvc-a, and a third, allowedsvc-c(processed aftersvc-b) mints the identical unqualified slugsvc-b's own importer node used. Confirmed the cross-repo edge carriescrossOrigins: ["svc-b", "svc-a"], the colliding node ends uporigin: "svc-c"after merge, andscopeGlobalGraph(..., new Set(['svc-b']))keeps both the now-allowedb/clientnode andsvc-a's owna/servicenode, while still dropping the cross-repo edge between them.detectCrossRepoEdgesdirectly against the compiled source with a hand-builtexistinggraph where the import edge's own tag (origin: "svc-b") disagrees with the CURRENT node at itsfromslug (origin: "svc-x", simulating a later collision) — confirmed the synthesized edge'scrossOriginscorrectly contains"svc-b"(the edge's own tag) and not"svc-x"(the stale node).DEPENDS_ONedge, plus per-repo files for a withheld and a colliding allowed repo, with noaggregateGlobalGraphcall so nothing gets re-tagged) and calledscopeGlobalGraphagainst the compiled source directly — confirmed it returnsnullinstead of silently keeping the unattributable edge.mergeGraphsdirectly against the compiled source with two edges sharing onefrom/to/relationidentity but differentcrossOrigins— confirmed the merged edge'scrossOriginsis the union of both (["svc-w","svc-z","svc-y"]), not just whichever side won the tie-break.aggregateGlobalGraphpipeline: withheldsvc-band allowedsvc-cboth independently import something matching allowedsvc-a's component under the identical generic importer slug. Confirmed the global graph ends up with exactly ONE merged edge carryingcrossOriginPairs: [["svc-b","svc-a"],["svc-c","svc-a"]](not two separate raw entries), thatscopeGlobalGraph(..., new Set(['svc-b']))keeps that edge (svc-c's pair is fully allowed), and that withholding bothsvc-bandsvc-ctogether removes it.svc-bwith acomponent/Appnode, called the realreconcileIwikiWithCodebaseagainst a doc containing`App`— confirmed the resulting edge carriesorigin: "svc-b". Then simulated a later collision (an allowedsvc-cclaiming the identical unqualified slug) and confirmedscopeGlobalGraph(..., new Set(['svc-b']))keeps the now-allowedcomponent/Appnode but still removes theMAPS_TOedge.scopeGlobalGraphdirectly against the compiled source with a hand-written edge carrying bothorigin: "svc-a"andcrossOriginPairs: [["svc-w","svc-z"]]— confirmed withholding onlysvc-wkeeps the edge (svc-a's own origin is independently allowed), and withholdingsvc-a,svc-w, andsvc-ztogether removes it.aggregateGlobalGraphpipeline against the compiled source:svc-a's own plain fact-level edge (a/x -> a/y) collides with a cross-repo edge independently synthesized from withheldsvc-wimporting allowedsvc-z's component. Confirmed the merged edge carriescrossOriginPairs: [["svc-a"],["svc-w","svc-z"]](not just the cross edge's pair, losingsvc-a's), and thatscopeGlobalGraph(..., new Set(['svc-w']))keeps the edge alive onsvc-a's independent claim.node dist/index.js codebase --extract <dir> --project widget --json), standalone, against a fresh single-file TypeScript fixture with no priorteamai importever run. Inspected the resultingteamwiki/.indices/graph-index.jsondirectly: every one of its 3 nodes and 1 edge carries"origin": "widget".A full project-wide
npx vitest runwas attempted multiple times (once per review round) but the host machine was resource-starved/low on memory from accumulated test-run processes; the harness killed it rather than let it run on unreliable output. Each time, the targeted suite above plus the real-CLI verification stand in for it; the one pre-existing slow/timeout-prone test outside this change (recall-attribution.test.ts) was independently confirmed unrelated via git-stash before/after comparison in the first round.Closes #912.
🤖 Generated with Claude Code