mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): layered-knowledge schema — layer, dependency table, stale columns
This commit is contained in:
parent
7a79fe93d0
commit
28284fbcac
@ -0,0 +1,48 @@
|
||||
package vip.mate.wiki.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* A dependency edge from an experience page to a fact page it relies on.
|
||||
* The reverse index ({@code depends_on_page_id}) drives stale propagation:
|
||||
* when a fact page changes, the experience pages depending on it are marked
|
||||
* stale. Stored by page id (never slug) so renames cannot break the edge.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Data
|
||||
@TableName("mate_wiki_page_dependency")
|
||||
public class WikiPageDependencyEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/** Knowledge base both pages belong to (cross-KB dependencies are rejected). */
|
||||
private Long kbId;
|
||||
|
||||
/** The dependent (experience) page. */
|
||||
private Long pageId;
|
||||
|
||||
/** The fact page being depended on. */
|
||||
private Long dependsOnPageId;
|
||||
|
||||
/** Dependency kind; {@code fact} for the fact→experience relation. */
|
||||
private String dependencyType;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
@ -71,6 +71,20 @@ public class WikiPageEntity {
|
||||
/** Profile version in effect when the page was generated or last validated. */
|
||||
private Integer profileVersion;
|
||||
|
||||
/** Knowledge layer derived from the pageType profile: {@code fact} / {@code experience}. */
|
||||
private String knowledgeLayer;
|
||||
|
||||
/** Fact page ids this (experience) page depends on, as a JSON array. Source of truth is the dependency table. */
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String dependsOnJson;
|
||||
|
||||
/** {@code 1} when an upstream fact page changed and this page may be out of date. */
|
||||
private Integer stale;
|
||||
|
||||
/** Why the page is stale (fact page id, time, reason) as JSON. */
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private String staleReasonJson;
|
||||
|
||||
/** Purpose hint for LLM ingest routing */
|
||||
private String purposeHint;
|
||||
|
||||
|
||||
@ -23,6 +23,13 @@ public class WikiPageTypeDef {
|
||||
/** Short description of what this page type represents. */
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* Knowledge layer this page type belongs to: {@code fact} ("what is") or
|
||||
* {@code experience} ("what it means"). Extensible — MVP recognises these
|
||||
* two; {@code null} means unspecified (treated as fact for retrieval).
|
||||
*/
|
||||
private String layer;
|
||||
|
||||
/** Field name → schema. Insertion order preserved for prompt rendering. */
|
||||
private Map<String, WikiFieldSchema> schema = new LinkedHashMap<>();
|
||||
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package vip.mate.wiki.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import vip.mate.wiki.model.WikiPageDependencyEntity;
|
||||
|
||||
/**
|
||||
* Mapper for {@link WikiPageDependencyEntity}.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Mapper
|
||||
public interface WikiPageDependencyMapper extends BaseMapper<WikiPageDependencyEntity> {
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
-- V135: Layered knowledge (fact / experience) + page dependency graph.
|
||||
--
|
||||
-- knowledge_layer is derived from the pageType profile (fact = "what is",
|
||||
-- experience = "what it means"). Experience pages depend on fact pages; the
|
||||
-- dependency table is the source of truth for stale propagation (reverse
|
||||
-- lookup by depends_on_page_id), with the page-local depends_on_json kept as
|
||||
-- a redundant copy. Stored by page id, never slug, so renames cannot break a
|
||||
-- dependency.
|
||||
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS knowledge_layer VARCHAR(16);
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS depends_on_json CLOB;
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS stale TINYINT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE mate_wiki_page ADD COLUMN IF NOT EXISTS stale_reason_json CLOB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mate_wiki_page_dependency (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
kb_id BIGINT NOT NULL,
|
||||
page_id BIGINT NOT NULL,
|
||||
depends_on_page_id BIGINT NOT NULL,
|
||||
dependency_type VARCHAR(32) NOT NULL DEFAULT 'fact',
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted INT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_wiki_page_dep
|
||||
ON mate_wiki_page_dependency (page_id, depends_on_page_id, dependency_type, deleted);
|
||||
-- Reverse lookup for stale propagation: "who depends on this fact page".
|
||||
CREATE INDEX IF NOT EXISTS idx_wiki_page_dep_reverse
|
||||
ON mate_wiki_page_dependency (kb_id, depends_on_page_id, deleted);
|
||||
@ -0,0 +1,44 @@
|
||||
-- V135: Layered knowledge (fact / experience) + page dependency graph.
|
||||
-- See the H2 file for rationale. MySQL uses INFORMATION_SCHEMA guards for the
|
||||
-- idempotent column adds.
|
||||
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'knowledge_layer');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN knowledge_layer VARCHAR(16)', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'depends_on_json');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN depends_on_json LONGTEXT', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'stale');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN stale TINYINT NOT NULL DEFAULT 0', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col_exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_page'
|
||||
AND COLUMN_NAME = 'stale_reason_json');
|
||||
SET @ddl := IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_wiki_page ADD COLUMN stale_reason_json LONGTEXT', 'SELECT 1');
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mate_wiki_page_dependency (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
kb_id BIGINT NOT NULL,
|
||||
page_id BIGINT NOT NULL,
|
||||
depends_on_page_id BIGINT NOT NULL,
|
||||
dependency_type VARCHAR(32) NOT NULL DEFAULT 'fact',
|
||||
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
deleted INT NOT NULL DEFAULT 0,
|
||||
UNIQUE KEY uk_wiki_page_dep (page_id, depends_on_page_id, dependency_type, deleted),
|
||||
KEY idx_wiki_page_dep_reverse (kb_id, depends_on_page_id, deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@ -0,0 +1,73 @@
|
||||
package vip.mate.wiki.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import vip.mate.wiki.model.WikiPageDependencyEntity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* Validates the page dependency table against H2: reverse lookup by
|
||||
* depends_on_page_id finds dependents, and the unique key prevents duplicate
|
||||
* edges.
|
||||
*/
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = {
|
||||
"spring.flyway.enabled=true",
|
||||
"spring.flyway.locations=classpath:db/migration/h2",
|
||||
"mateclaw.feature-flag.refresh-ms=999999"
|
||||
}
|
||||
)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
class WikiPageDependencyMapperE2ETest {
|
||||
|
||||
@Autowired
|
||||
private WikiPageDependencyMapper mapper;
|
||||
|
||||
private static final java.util.concurrent.atomic.AtomicLong SEQ =
|
||||
new java.util.concurrent.atomic.AtomicLong(System.nanoTime());
|
||||
|
||||
private WikiPageDependencyEntity edge(long kb, long page, long dependsOn) {
|
||||
WikiPageDependencyEntity e = new WikiPageDependencyEntity();
|
||||
e.setKbId(kb);
|
||||
e.setPageId(page);
|
||||
e.setDependsOnPageId(dependsOn);
|
||||
e.setDependencyType("fact");
|
||||
e.setCreateTime(LocalDateTime.now());
|
||||
e.setUpdateTime(LocalDateTime.now());
|
||||
return e;
|
||||
}
|
||||
|
||||
@Test
|
||||
void reverseLookupFindsDependents() {
|
||||
long kb = SEQ.incrementAndGet();
|
||||
long fact = SEQ.incrementAndGet();
|
||||
long expA = SEQ.incrementAndGet();
|
||||
long expB = SEQ.incrementAndGet();
|
||||
mapper.insert(edge(kb, expA, fact));
|
||||
mapper.insert(edge(kb, expB, fact));
|
||||
|
||||
List<WikiPageDependencyEntity> dependents = mapper.selectList(
|
||||
Wrappers.<WikiPageDependencyEntity>lambdaQuery()
|
||||
.eq(WikiPageDependencyEntity::getKbId, kb)
|
||||
.eq(WikiPageDependencyEntity::getDependsOnPageId, fact));
|
||||
assertEquals(2, dependents.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateEdgeRejected() {
|
||||
long kb = SEQ.incrementAndGet();
|
||||
long page = SEQ.incrementAndGet();
|
||||
long fact = SEQ.incrementAndGet();
|
||||
mapper.insert(edge(kb, page, fact));
|
||||
assertThrows(Exception.class, () -> mapper.insert(edge(kb, page, fact)));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user