mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +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. */
|
/** Profile version in effect when the page was generated or last validated. */
|
||||||
private Integer profileVersion;
|
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 */
|
/** Purpose hint for LLM ingest routing */
|
||||||
private String purposeHint;
|
private String purposeHint;
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,13 @@ public class WikiPageTypeDef {
|
|||||||
/** Short description of what this page type represents. */
|
/** Short description of what this page type represents. */
|
||||||
private String description;
|
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. */
|
/** Field name → schema. Insertion order preserved for prompt rendering. */
|
||||||
private Map<String, WikiFieldSchema> schema = new LinkedHashMap<>();
|
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