Skip to content

Knowledge base scopes + tags + shared common vault (ADT-234) - #39

Merged
olehsvyrydov merged 9 commits into
feat/dart-interactivefrom
feat/dart-knowledge
Jun 9, 2026
Merged

Knowledge base scopes + tags + shared common vault (ADT-234)#39
olehsvyrydov merged 9 commits into
feat/dart-interactivefrom
feat/dart-knowledge

Conversation

@olehsvyrydov

Copy link
Copy Markdown
Owner

Rename Base → Knowledge and add scopes/tags (design /aura+/apex; arch + HARD secops gated).

What

  • Front-matter scope/tag model — each note carries scope (project|common) + stack[]/kind, parsed from markdown front-matter (source of truth, so the hub projection and the memory recall hooks agree).
  • Shared common vault (~/.aidevteam/kb-common/) — one physical file surfaced into every matching project; cross-type match: a Java project sees Java + stack-agnostic common, never another stack's.
  • Scoped add — scope is a server-validated enum selecting one of two server-known vaults (never a client path); reuses the contained/O_EXCL/size-capped/guarded write.
  • Knowledge panel UI — scope toggle (This project / Common / All + counts), stack/kind filters, per-doc scope badge + tags, honest local-first + filename-only copy.
  • hub↔memory parity locked by a shared fixture (byte-identical results).

Security (HARD gate, verified)

Common-vault containment (sibling-prefix + symlink-escape refused), O_EXCL, size/text caps, write-guard on every new path, enum-not-path scope, cross-stack/cross-project isolation, front-matter never-throws + proto-safe, all untrusted text escaped (no innerHTML). Conditions C-201…C-242 / negatives N-201…N-219 all green.

Tests

283 hub + 25 memory (tsc clean) + 417 cockpit; build succeeds. Internal /rev passed (0 blocking).

🤖 Generated with Claude Code

…n vault

Rename the Base panel to Knowledge and give each note a scope (project or
common) plus stack/kind tags carried in markdown front-matter (the source
of truth, so the hub and the memory hooks agree). Project notes stay in the
project's vault; common notes live in one shared user-level vault and
surface into every project that matches their stack — a Java project sees
Java and stack-agnostic common knowledge, never another stack's specifics.

Adding a note takes a scope (a server-validated enum that selects the
vault, never a client path) and tags, reusing the realpath-contained,
no-overwrite, size-capped, guarded write against the chosen vault root. The
match predicate and the front-matter parser are a single source of truth,
parity-locked between the hub projection and the memory recall filter; a
malformed front-matter never throws and drops prototype-polluting keys.
`global` reads as `common`; nothing is uploaded.

Refs: ADT-234
Copilot AI review requested due to automatic review settings June 9, 2026 02:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR upgrades the existing project “Base” knowledge surface into a scoped Knowledge system: notes gain scope (project/common) plus stack/kind tags stored in markdown front-matter, with a shared common vault that is merged into each project’s knowledge view under a strict stack-matching rule. It includes UI updates in the cockpit, backend support in the hub, and hub↔memory parity tests to keep projection and recall behavior aligned.

Changes:

  • Introduces the Knowledge scope/tag data model (front-matter parsing + scope/stack match predicate) and exposes a merged state.knowledge projection (project + common).
  • Extends kb/add to support a server-validated scope enum plus optional stack/kind tags, writing self-describing notes with front-matter (including common-vault writes).
  • Updates the cockpit Knowledge panel and add form to support scope toggling, chips/filters, and adds parity + negative/security tests across hub and memory.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
studio/cockpit/src/app/shell/project-shell.component.ts Prefer state.knowledge for the panel, fallback to state.base.
studio/cockpit/src/app/shell/project-shell.component.spec.ts Updates rich state fixtures to the new knowledge shape.
studio/cockpit/src/app/shell/glyph.component.ts Adds new glyph names + SVGs for scope and tag chips.
studio/cockpit/src/app/shell/base-panel.component.ts Reworks “Base” panel UI into scoped Knowledge list with filters/chips.
studio/cockpit/src/app/shell/base-panel.component.spec.ts Adds tests for scope toggle, chips, filters, and XSS escaping.
studio/cockpit/src/app/shell/add-note-form.component.ts Adds scoped add UI + stack/kind tagging; posts new fields.
studio/cockpit/src/app/shell/add-note-form.component.spec.ts Updates add form tests for scope + tag payload and honesty copy.
studio/cockpit/src/app/projects/project-card.component.spec.ts Updates fixtures to new knowledge shape and asserts card doesn’t show KB count.
studio/cockpit/src/app/core/projects.store.ts Counts docs from knowledge per-scope counts.
studio/cockpit/src/app/core/models.ts Introduces Knowledge types and documents base/knowledge back-compat intent.
studio/cockpit/src/app/core/control-plane.service.ts Extends kb/add input/body to include scope/stack/kind.
hub/lib/knowledge.js Adds bounded front-matter parser + canonical scopeMatches predicate + stack detection + common vault root resolution.
hub/lib/write.js Extends addKbNote to scoped writes and emits front-matter header.
hub/lib/state.js Adds merged knowledge projection + vault scanning w/ parsed front-matter.
hub/lib/api.js Accepts scope/stack/kind on kb/add and forwards to writer.
hub/lib/scope-fixtures.json Shared fixture truth table for scope/stack visibility parity.
hub/test/knowledge.test.js Unit tests for front-matter parsing + scopeMatches + project stack precedence.
hub/test/scope-parity.test.js Parity test: evaluates JS + TS mirror over shared fixtures for identical results.
hub/test/mutation-guard.test.js Adds guard negatives for scoped kb/add (project + common).
hub/test/kb-write.test.js Adjusts expectations for front-matter header inclusion.
hub/test/kb-scope-write.test.js Adds extensive scoped write security/negative tests for common vault and scope enum.
hub/test/kb-projection.test.js End-to-end tests for merged projection, isolation, and honest method.
claude/memory/src/lib/knowledge-match.ts TS mirror of hub scopeMatches predicate (with global→common alias).
claude/memory/test/knowledge-match.test.ts Validates TS mirror against shared hub fixture table.
claude/memory/src/lib/project-stack.ts Mirrors hub project stack resolution logic (manual > auto > any).
claude/memory/src/stores/collections.ts Adds stack as a filterable payload field.
claude/memory/src/hooks/restore-context.ts Narrows global/common recall by project stack via shared predicate.
docs/sprints/sprint-06-knowledge-scopes/README.md Sprint-level overview and gate plan for knowledge scopes work.
docs/sprints/sprint-06-knowledge-scopes/DECISION_LOG.md Records decisions for canonical scope term, stack declaration, and deferrals.
docs/sprints/sprint-06-knowledge-scopes/TICKETS.md Behavior-only acceptance criteria for ADT-234 (and related follow-ons).
docs/sprints/sprint-06-knowledge-scopes/reviews/rev-knowledge-234.md Internal review artifact documenting verification against SECOPS conditions/tests.
docs/sprints/sprint-06-knowledge-scopes/approvals/arch-knowledge-scopes.md Architecture approval writeup for ADT-234/235.
docs/sprints/sprint-06-knowledge-scopes/approvals/secops-knowledge-scopes.md SECOPS gate conditions and negative test checklist for knowledge scopes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +117 to +120
@if (visibleDocs().length) {
<ul class="docs" aria-label="Knowledge documents">
@for (doc of visibleDocs(); track doc.name) {
<li class="doc" data-testid="knowledge-doc">

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 78af6f3 + 9677b82 — good catches. The common-vault READ path was a real gap: it now uses the same containment-validated resolver as the write path (a commonVaultDir override outside $HOME is no longer read; proven by a new negative test). Knowledge rows now track by scope+file (no cross-vault name collision); the misleading BaseView=KnowledgeView alias is removed and consumers use the accurate types; the resolveCommonKbDir comment, the addKbNote contract doc, and the dead parseFrontMatter import are all corrected.

Comment thread hub/lib/write.js Outdated
Comment thread hub/lib/write.js Outdated
Comment on lines +160 to +165
// Resolve the user-level common-vault directory. The intended root comes from the
// knowledge module (default ~/.aidevteam/kb-common, or a bounded config override);
// it is created if absent (like the project default) and realpath-resolved. The
// resolved real path must be contained to the home root UNLESS an absolute override
// deliberately points elsewhere — in which case the override path is itself the
// containment root and must resolve to a real directory. Returns the realpath of the

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 78af6f3 + 9677b82 — good catches. The common-vault READ path was a real gap: it now uses the same containment-validated resolver as the write path (a commonVaultDir override outside $HOME is no longer read; proven by a new negative test). Knowledge rows now track by scope+file (no cross-vault name collision); the misleading BaseView=KnowledgeView alias is removed and consumers use the accurate types; the resolveCommonKbDir comment, the addKbNote contract doc, and the dead parseFrontMatter import are all corrected.

Comment thread hub/lib/state.js Outdated
Comment on lines 326 to 331
// The common-vault doc records. Read-only scan of the SINGLE shared vault; the
// resolved root must be a real directory contained to the user home (or its bounded
// override), else the scan is empty (never reads an uncontained path).
function readCommonKb() {
let root;
try {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 78af6f3 + 9677b82 — good catches. The common-vault READ path was a real gap: it now uses the same containment-validated resolver as the write path (a commonVaultDir override outside $HOME is no longer read; proven by a new negative test). Knowledge rows now track by scope+file (no cross-vault name collision); the misleading BaseView=KnowledgeView alias is removed and consumers use the accurate types; the resolveCommonKbDir comment, the addKbNote contract doc, and the dead parseFrontMatter import are all corrected.

Comment thread studio/cockpit/src/app/core/models.ts Outdated
Comment on lines +124 to +130
/**
* The shape carried into the Knowledge panel. The hub serialises the merged projection under
* `state.knowledge`; older state still carries the pre-scope `base` shape (counts by index state),
* so {@link KnowledgeView} is the panel's contract and the shell adapts whichever the state holds.
*/
export type BaseDoc = KnowledgeDoc;
export type BaseView = KnowledgeView;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 78af6f3 + 9677b82 — good catches. The common-vault READ path was a real gap: it now uses the same containment-validated resolver as the write path (a commonVaultDir override outside $HOME is no longer read; proven by a new negative test). Knowledge rows now track by scope+file (no cross-vault name collision); the misleading BaseView=KnowledgeView alias is removed and consumers use the accurate types; the resolveCommonKbDir comment, the addKbNote contract doc, and the dead parseFrontMatter import are all corrected.

Comment on lines 251 to 255
@@ -248,7 +255,11 @@ export class ControlPlaneService {
* This is an additive create, not a CAS mutation, so there is no `expectedRev` and no conflict.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 78af6f3 + 9677b82 — good catches. The common-vault READ path was a real gap: it now uses the same containment-validated resolver as the write path (a commonVaultDir override outside $HOME is no longer read; proven by a new negative test). Knowledge rows now track by scope+file (no cross-vault name collision); the misleading BaseView=KnowledgeView alias is removed and consumers use the accurate types; the resolveCommonKbDir comment, the addKbNote contract doc, and the dead parseFrontMatter import are all corrected.

The common-vault read now uses the same containment-validated resolver as
the write path (proven a commonVaultDir override outside $HOME is not read);
correct the resolveCommonKbDir comment to match the contained-to-$HOME
behavior; drop the unused parseFrontMatter import.
…ls, addKbNote contract doc

Track Knowledge rows by scope+file so same-named project/common notes don't
share a DOM node; make the client base/knowledge types match the actual wire
shapes; document that addKbNote now sends scope/stack/kind.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 2 comments.

Comment thread studio/cockpit/src/app/shell/base-panel.component.ts
Comment on lines +73 to +84
const globalHitsRaw = await store.query("dev-rules", qv, { scope: "global" }, 5);
// Narrow cross-project (global/common) rows by the project's declared stack via
// the SHARED scope predicate — the same any-wildcard/intersection rule the hub
// panel applies — so a stack-specific shared rule never leaks into a project of
// a different stack. A row with no stack tag is treated as "any" (recalled).
const declaredStack = projectStackOf(cwd);
const globalHits = globalHitsRaw.filter((h) => {
const tags = (h.payload && Array.isArray((h.payload as Record<string, unknown>).stack)
? ((h.payload as Record<string, unknown>).stack as string[])
: ["any"]);
return scopeMatches({ scope: "common", status: "approved-common", stack: tags }, { stack: declaredStack });
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e2b8cc2 + 5422edf — the redundant literal 'any' is excluded from the Stack filter options (the default empty option already means all/any; any-tagged docs still show under the default); and the global dev-rules recall now applies the stack/scope filter to a bounded candidate pool BEFORE taking the top 5, so a stack-mismatched top hit no longer under-fills the section.

The Stack filter's default empty option already means all/any, so the
literal any tag is excluded from the option list to avoid a duplicate;
any-tagged docs still show under the default.
Apply the stack/scope match to the global-rule candidates before taking
the top 5, so a stack-mismatched top result no longer under-fills the
Global Dev Rules section; candidate fetch stays bounded.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 5 comments.

Comment on lines +55 to +63
<div class="scope" role="radiogroup" aria-label="Knowledge scope">
<button
type="button"
class="seg__opt"
data-testid="knowledge-scope-project"
role="radio"
[attr.aria-checked]="scopeFilter() === 'project'"
[class.seg__opt--on]="scopeFilter() === 'project'"
(click)="setScope('project')"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in bd0a1c3 + 965363e — both scope radiogroups now support ArrowLeft/Right/Up/Down roving selection + focus (matching the preset radiogroup pattern), with roving tabindex; the two test fixtures now match the real wire shapes (method 'filename-only'; common doc file root-relative); and the parseScalarValue comment is corrected to state it does not strip inline comments.

Comment on lines +93 to +104
<fieldset class="scope" data-testid="note-scope" role="radiogroup" aria-label="Scope">
<legend class="lbl">Scope</legend>
<div class="seg">
<button
type="button"
class="seg__opt"
data-testid="note-scope-project"
role="radio"
[attr.aria-checked]="scope() === 'project'"
[class.seg__opt--on]="scope() === 'project'"
(click)="setScope('project')"
>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in bd0a1c3 + 965363e — both scope radiogroups now support ArrowLeft/Right/Up/Down roving selection + focus (matching the preset radiogroup pattern), with roving tabindex; the two test fixtures now match the real wire shapes (method 'filename-only'; common doc file root-relative); and the parseScalarValue comment is corrected to state it does not strip inline comments.

Comment thread studio/cockpit/src/app/projects/project-card.component.spec.ts Outdated
Comment thread studio/cockpit/src/app/shell/add-note-form.component.spec.ts Outdated
Comment thread hub/lib/knowledge.js
Comment on lines +81 to +86
function parseScalarValue(raw) {
let s = String(raw == null ? '' : raw).trim();
if (s === '') return undefined; // a bare `key:` opens a nested block → reject
// strip a trailing inline comment that is clearly a comment (not inside quotes)
if (s[0] === '[') {
const close = s.lastIndexOf(']');

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in bd0a1c3 + 965363e — both scope radiogroups now support ArrowLeft/Right/Up/Down roving selection + focus (matching the preset radiogroup pattern), with roving tabindex; the two test fixtures now match the real wire shapes (method 'filename-only'; common doc file root-relative); and the parseScalarValue comment is corrected to state it does not strip inline comments.

The front-matter scalar parser does not strip inline comments; the value
after the colon is taken and trimmed in full, so a trailing '#' is kept.
The comment now matches the implementation.
…est fixtures

The Knowledge scope toggle and the add-note scope picker now support
ArrowLeft/Right/Up/Down roving selection like the other segmented radios;
align two test fixtures (method filename-only, common doc file path) with
the real wire shapes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Comment on lines 308 to +319
async submit(): Promise<void> {
if (!this.canSubmit()) return;
this.lifecycle.set('saving');
this.message.set('');
const res = await this.cp.addKbNote({ title: this.title().trim(), body: this.body() });
const stack = this.stack() ? [this.stack()] : ['any'];
const res = await this.cp.addKbNote({
title: this.title().trim(),
body: this.body(),
scope: this.scope(),
stack,
kind: this.kind(),
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in ec7ac69 + 0834f02 — selectGlobalRules now passes each candidate's actual status to scopeMatches, so pending/rejected global rows are no longer recalled (only approved); the collections comment now states the real vec0 columns and that stack is matched post-fetch in the hook (not a store column); and the add-note form blocks an empty/whitespace body before posting.

Comment on lines +66 to +71
const matching = candidates.filter((h) => {
const tags = (h.payload && Array.isArray((h.payload as Record<string, unknown>).stack)
? ((h.payload as Record<string, unknown>).stack as string[])
: ["any"]);
return scopeMatches({ scope: "common", status: "approved-common", stack: tags }, { stack: declaredStack });
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in ec7ac69 + 0834f02 — selectGlobalRules now passes each candidate's actual status to scopeMatches, so pending/rejected global rows are no longer recalled (only approved); the collections comment now states the real vec0 columns and that stack is matched post-fetch in the hook (not a store column); and the add-note form blocks an empty/whitespace body before posting.

Comment thread claude/memory/src/stores/collections.ts Outdated
Comment on lines +18 to +22
* Payload fields we filter on — indexed in Qdrant, real columns in sqlite-vec.
* `stack` is the cross-type knowledge-scope dimension: a common/global row may carry
* a stack tag, and recall narrows it against the project's declared stack via the
* shared scope predicate (see lib/knowledge-match.ts), with an "any" wildcard.
*/

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in ec7ac69 + 0834f02 — selectGlobalRules now passes each candidate's actual status to scopeMatches, so pending/rejected global rows are no longer recalled (only approved); the collections comment now states the real vec0 columns and that stack is matched post-fetch in the hook (not a store column); and the add-note form blocks an empty/whitespace body before posting.

…umn comment

selectGlobalRules now matches each candidate's actual status so pending/rejected
global rows are not recalled; the collections comment now states the real vec0
columns and that stack is matched post-fetch in the hook, not filtered by the store.
body: the add-note form now requires a non-empty body before posting, matching the title guard, so the UI no longer fires a request the hub will reject.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Comment thread hub/lib/write.js
Comment on lines +171 to +181
const home = aidevteamHome();
const intended = commonVaultRoot();
const isDefault = intended === path.join(home, 'kb-common');

if (isDefault) {
try { fs.mkdirSync(intended, { recursive: true }); } catch { return null; }
} else {
// a non-existent / non-directory override is refused rather than created or followed
let real;
try { real = fs.realpathSync(intended); } catch { return null; }
try { if (!fs.statSync(real).isDirectory()) return null; } catch { return null; }
Comment on lines +19 to +22
import type { ScoredPoint, VectorStore } from "../types.ts";
import { formatContext, readStdinJson, withDeadline } from "./common.ts";
import { scopeMatches } from "../lib/knowledge-match.ts";
import { projectStackOf } from "../lib/project-stack.ts";
Comment on lines +148 to +151
/** True when this module is the process entrypoint (run directly as the hook). */
function isEntrypoint(): boolean {
return import.meta.url === `file://${process.argv[1]}`;
}
@olehsvyrydov
olehsvyrydov merged commit 060307a into feat/dart-interactive Jun 9, 2026
1 check passed
@olehsvyrydov
olehsvyrydov deleted the feat/dart-knowledge branch June 9, 2026 03:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants