zam-core 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +3275 -1737
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +106 -5
- package/dist/index.js +487 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -616,6 +616,24 @@ CREATE TABLE IF NOT EXISTS session_syntheses (
|
|
|
616
616
|
PRIMARY KEY (session_id, token_id)
|
|
617
617
|
);
|
|
618
618
|
|
|
619
|
+
-- Sources: textbook files, web links, or scan paths
|
|
620
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
621
|
+
id TEXT PRIMARY KEY,
|
|
622
|
+
type TEXT NOT NULL CHECK (type IN ('file', 'web', 'scan')),
|
|
623
|
+
uri TEXT NOT NULL UNIQUE,
|
|
624
|
+
content TEXT,
|
|
625
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
-- Token sources: mapping between tokens and their sources
|
|
629
|
+
CREATE TABLE IF NOT EXISTS token_sources (
|
|
630
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
631
|
+
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
632
|
+
excerpt TEXT NOT NULL DEFAULT '',
|
|
633
|
+
page_number TEXT,
|
|
634
|
+
PRIMARY KEY (token_id, source_id)
|
|
635
|
+
);
|
|
636
|
+
|
|
619
637
|
-- User configuration
|
|
620
638
|
CREATE TABLE IF NOT EXISTS user_config (
|
|
621
639
|
key TEXT PRIMARY KEY,
|
|
@@ -943,6 +961,24 @@ async function runMigrations(db) {
|
|
|
943
961
|
PRIMARY KEY (session_id, token_id)
|
|
944
962
|
)
|
|
945
963
|
`);
|
|
964
|
+
await db.exec(`
|
|
965
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
966
|
+
id TEXT PRIMARY KEY,
|
|
967
|
+
type TEXT NOT NULL CHECK (type IN ('file', 'web', 'scan')),
|
|
968
|
+
uri TEXT NOT NULL UNIQUE,
|
|
969
|
+
content TEXT,
|
|
970
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
971
|
+
)
|
|
972
|
+
`);
|
|
973
|
+
await db.exec(`
|
|
974
|
+
CREATE TABLE IF NOT EXISTS token_sources (
|
|
975
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
976
|
+
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
977
|
+
excerpt TEXT NOT NULL DEFAULT '',
|
|
978
|
+
page_number TEXT,
|
|
979
|
+
PRIMARY KEY (token_id, source_id)
|
|
980
|
+
)
|
|
981
|
+
`);
|
|
946
982
|
}
|
|
947
983
|
|
|
948
984
|
// src/kernel/db/snapshot.ts
|
|
@@ -1646,6 +1682,415 @@ async function listTokens(db, options) {
|
|
|
1646
1682
|
"SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
|
|
1647
1683
|
).all();
|
|
1648
1684
|
}
|
|
1685
|
+
function slugify(text) {
|
|
1686
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1687
|
+
}
|
|
1688
|
+
async function generateTokenSlug(db, domain, concept, question) {
|
|
1689
|
+
const baseText = question && question.trim().length > 0 ? question : concept;
|
|
1690
|
+
const cleanDomain = slugify(domain || "");
|
|
1691
|
+
const cleanBase = slugify(baseText);
|
|
1692
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
1693
|
+
if (baseSlug.length > 60) {
|
|
1694
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
1695
|
+
}
|
|
1696
|
+
if (!baseSlug) {
|
|
1697
|
+
baseSlug = "token";
|
|
1698
|
+
}
|
|
1699
|
+
let slug = baseSlug;
|
|
1700
|
+
let counter = 1;
|
|
1701
|
+
while (true) {
|
|
1702
|
+
const existing = await db.prepare("SELECT id FROM tokens WHERE slug = ?").get(slug);
|
|
1703
|
+
if (!existing) {
|
|
1704
|
+
return slug;
|
|
1705
|
+
}
|
|
1706
|
+
const suffix = `-${counter}`;
|
|
1707
|
+
slug = baseSlug.slice(0, 60 - suffix.length).replace(/-$/, "") + suffix;
|
|
1708
|
+
counter++;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
async function listPersonalCards(db, userId, options) {
|
|
1712
|
+
let sql = `
|
|
1713
|
+
SELECT
|
|
1714
|
+
t.id AS tokenId,
|
|
1715
|
+
t.slug,
|
|
1716
|
+
t.concept,
|
|
1717
|
+
t.domain,
|
|
1718
|
+
t.bloom_level AS bloomLevel,
|
|
1719
|
+
t.context,
|
|
1720
|
+
t.symbiosis_mode AS symbiosisMode,
|
|
1721
|
+
t.source_link AS sourceLink,
|
|
1722
|
+
t.question,
|
|
1723
|
+
t.created_at AS createdAt,
|
|
1724
|
+
t.updated_at AS updatedAt,
|
|
1725
|
+
c.id AS cardId,
|
|
1726
|
+
c.state,
|
|
1727
|
+
c.due_at AS dueAt,
|
|
1728
|
+
c.stability,
|
|
1729
|
+
c.difficulty,
|
|
1730
|
+
c.reps,
|
|
1731
|
+
c.lapses,
|
|
1732
|
+
c.elapsed_days AS elapsedDays,
|
|
1733
|
+
c.scheduled_days AS scheduledDays,
|
|
1734
|
+
c.blocked
|
|
1735
|
+
FROM tokens t
|
|
1736
|
+
INNER JOIN cards c ON c.token_id = t.id AND c.user_id = ?
|
|
1737
|
+
WHERE t.deprecated_at IS NULL
|
|
1738
|
+
`;
|
|
1739
|
+
const values = [userId];
|
|
1740
|
+
if (options?.domain) {
|
|
1741
|
+
sql += " AND t.domain = ?";
|
|
1742
|
+
values.push(options.domain);
|
|
1743
|
+
}
|
|
1744
|
+
if (options?.query) {
|
|
1745
|
+
const terms = options.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1746
|
+
for (const term of terms) {
|
|
1747
|
+
sql += ` AND (lower(t.slug) LIKE ? OR lower(t.concept) LIKE ? OR lower(t.domain) LIKE ? OR lower(t.question) LIKE ?)`;
|
|
1748
|
+
const pattern = `%${term}%`;
|
|
1749
|
+
values.push(pattern, pattern, pattern, pattern);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
sql += " ORDER BY t.created_at DESC";
|
|
1753
|
+
const rows = await db.prepare(sql).all(...values);
|
|
1754
|
+
return rows;
|
|
1755
|
+
}
|
|
1756
|
+
async function importCurriculumCards(db, userId, cards) {
|
|
1757
|
+
let createdCount = 0;
|
|
1758
|
+
let ensuredCount = 0;
|
|
1759
|
+
await db.transaction(async (tx) => {
|
|
1760
|
+
for (const card of cards) {
|
|
1761
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
1762
|
+
if (bloom < 1 || bloom > 5) {
|
|
1763
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1764
|
+
}
|
|
1765
|
+
let symbiosisMode = null;
|
|
1766
|
+
if (card.symbiosis_mode) {
|
|
1767
|
+
if (!["shadowing", "copilot", "autonomy", "none"].includes(
|
|
1768
|
+
card.symbiosis_mode
|
|
1769
|
+
)) {
|
|
1770
|
+
throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
|
|
1771
|
+
}
|
|
1772
|
+
symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
|
|
1773
|
+
}
|
|
1774
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
1775
|
+
const cleanDomain = slugify(card.domain || "");
|
|
1776
|
+
const cleanBase = slugify(baseText);
|
|
1777
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
1778
|
+
if (baseSlug.length > 60) {
|
|
1779
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
1780
|
+
}
|
|
1781
|
+
if (!baseSlug) {
|
|
1782
|
+
baseSlug = "token";
|
|
1783
|
+
}
|
|
1784
|
+
let token = await getTokenBySlug(tx, baseSlug);
|
|
1785
|
+
if (!token) {
|
|
1786
|
+
const finalSlug = await generateTokenSlug(
|
|
1787
|
+
tx,
|
|
1788
|
+
card.domain,
|
|
1789
|
+
card.concept,
|
|
1790
|
+
card.question
|
|
1791
|
+
);
|
|
1792
|
+
token = await createToken(tx, {
|
|
1793
|
+
slug: finalSlug,
|
|
1794
|
+
concept: card.concept,
|
|
1795
|
+
domain: card.domain,
|
|
1796
|
+
bloom_level: bloom,
|
|
1797
|
+
context: card.context || "",
|
|
1798
|
+
symbiosis_mode: symbiosisMode,
|
|
1799
|
+
source_link: card.source_link || null,
|
|
1800
|
+
question: card.question || null
|
|
1801
|
+
});
|
|
1802
|
+
createdCount++;
|
|
1803
|
+
}
|
|
1804
|
+
const existingCard = await getCard(tx, token.id, userId);
|
|
1805
|
+
if (!existingCard) {
|
|
1806
|
+
await ensureCard(tx, token.id, userId);
|
|
1807
|
+
ensuredCount++;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
return { createdCount, ensuredCount };
|
|
1812
|
+
}
|
|
1813
|
+
async function confirmCardSplit(db, userId, originalSlug, action, originalQuestion, originalConcept, proposals) {
|
|
1814
|
+
if (action !== "block" && action !== "remove") {
|
|
1815
|
+
throw new Error(`Invalid split action: ${action}`);
|
|
1816
|
+
}
|
|
1817
|
+
if (proposals.length < 2 || proposals.length > 4) {
|
|
1818
|
+
throw new Error("A card split requires between 2 and 4 proposals");
|
|
1819
|
+
}
|
|
1820
|
+
const originalToken = await getTokenBySlug(db, originalSlug);
|
|
1821
|
+
if (!originalToken) {
|
|
1822
|
+
throw new Error(`Original token not found: ${originalSlug}`);
|
|
1823
|
+
}
|
|
1824
|
+
let createdCount = 0;
|
|
1825
|
+
let ensuredCount = 0;
|
|
1826
|
+
await db.transaction(async (tx) => {
|
|
1827
|
+
const originalCard = await getCard(tx, originalToken.id, userId);
|
|
1828
|
+
if (!originalCard) {
|
|
1829
|
+
throw new Error(
|
|
1830
|
+
`Card not found for token ${originalSlug} and user ${userId}`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
const proposalTokens = [];
|
|
1834
|
+
for (const card of proposals) {
|
|
1835
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
1836
|
+
if (bloom < 1 || bloom > 5) {
|
|
1837
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1838
|
+
}
|
|
1839
|
+
let symbiosisMode = null;
|
|
1840
|
+
if (card.symbiosis_mode) {
|
|
1841
|
+
if (!["shadowing", "copilot", "autonomy", "none"].includes(
|
|
1842
|
+
card.symbiosis_mode
|
|
1843
|
+
)) {
|
|
1844
|
+
throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
|
|
1845
|
+
}
|
|
1846
|
+
symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
|
|
1847
|
+
}
|
|
1848
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
1849
|
+
const cleanDomain = slugify(card.domain || "");
|
|
1850
|
+
const cleanBase = slugify(baseText);
|
|
1851
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
1852
|
+
if (baseSlug.length > 60) {
|
|
1853
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
1854
|
+
}
|
|
1855
|
+
if (!baseSlug) {
|
|
1856
|
+
baseSlug = "token";
|
|
1857
|
+
}
|
|
1858
|
+
let token = await getTokenBySlug(tx, baseSlug);
|
|
1859
|
+
if (!token) {
|
|
1860
|
+
const finalSlug = await generateTokenSlug(
|
|
1861
|
+
tx,
|
|
1862
|
+
card.domain,
|
|
1863
|
+
card.concept,
|
|
1864
|
+
card.question
|
|
1865
|
+
);
|
|
1866
|
+
token = await createToken(tx, {
|
|
1867
|
+
slug: finalSlug,
|
|
1868
|
+
concept: card.concept,
|
|
1869
|
+
domain: card.domain,
|
|
1870
|
+
bloom_level: bloom,
|
|
1871
|
+
context: card.context || "",
|
|
1872
|
+
symbiosis_mode: symbiosisMode,
|
|
1873
|
+
source_link: card.source_link || originalToken.source_link || null,
|
|
1874
|
+
question: card.question || null
|
|
1875
|
+
});
|
|
1876
|
+
createdCount++;
|
|
1877
|
+
}
|
|
1878
|
+
if (token.id === originalToken.id) {
|
|
1879
|
+
throw new Error("A token cannot be a prerequisite of itself");
|
|
1880
|
+
}
|
|
1881
|
+
await assertPrerequisiteDoesNotCreateCycle(
|
|
1882
|
+
tx,
|
|
1883
|
+
originalToken.id,
|
|
1884
|
+
token.id
|
|
1885
|
+
);
|
|
1886
|
+
proposalTokens.push(token);
|
|
1887
|
+
const existingCard = await getCard(tx, token.id, userId);
|
|
1888
|
+
if (!existingCard) {
|
|
1889
|
+
await ensureCard(tx, token.id, userId);
|
|
1890
|
+
ensuredCount++;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
if (action === "block") {
|
|
1894
|
+
await tx.prepare(
|
|
1895
|
+
"UPDATE tokens SET question = ?, concept = ?, updated_at = ? WHERE id = ?"
|
|
1896
|
+
).run(
|
|
1897
|
+
originalQuestion || null,
|
|
1898
|
+
originalConcept,
|
|
1899
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1900
|
+
originalToken.id
|
|
1901
|
+
);
|
|
1902
|
+
for (const propToken of proposalTokens) {
|
|
1903
|
+
await tx.prepare(
|
|
1904
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
1905
|
+
).run(originalToken.id, propToken.id);
|
|
1906
|
+
}
|
|
1907
|
+
await tx.prepare(
|
|
1908
|
+
"UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
|
|
1909
|
+
).run(originalToken.id, userId);
|
|
1910
|
+
for (const propToken of proposalTokens) {
|
|
1911
|
+
const card = await ensureCard(tx, propToken.id, userId);
|
|
1912
|
+
if (card.blocked === 1) {
|
|
1913
|
+
const prereqOfPrereq = await tx.prepare(
|
|
1914
|
+
"SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
|
|
1915
|
+
).get(propToken.id);
|
|
1916
|
+
if (prereqOfPrereq.n === 0) {
|
|
1917
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1918
|
+
await tx.prepare("UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?").run(now, card.id);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
} else if (action === "remove") {
|
|
1923
|
+
await deleteCardForUser(tx, originalToken.id, userId);
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
return { createdCount, ensuredCount };
|
|
1927
|
+
}
|
|
1928
|
+
async function confirmFoundations(db, userId, originalSlug, proposals) {
|
|
1929
|
+
const originalToken = await getTokenBySlug(db, originalSlug);
|
|
1930
|
+
if (!originalToken) {
|
|
1931
|
+
throw new Error(`Original token not found: ${originalSlug}`);
|
|
1932
|
+
}
|
|
1933
|
+
let createdCount = 0;
|
|
1934
|
+
let linkedCount = 0;
|
|
1935
|
+
await db.transaction(async (tx) => {
|
|
1936
|
+
const originalCard = await getCard(tx, originalToken.id, userId);
|
|
1937
|
+
if (!originalCard) {
|
|
1938
|
+
throw new Error(
|
|
1939
|
+
`Card not found for token ${originalSlug} and user ${userId}`
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
for (const card of proposals) {
|
|
1943
|
+
let targetTokenId;
|
|
1944
|
+
if (card.exists && card.slug) {
|
|
1945
|
+
const existingToken = await getTokenBySlug(tx, card.slug);
|
|
1946
|
+
if (!existingToken) {
|
|
1947
|
+
throw new Error(`Prerequisite token not found by slug: ${card.slug}`);
|
|
1948
|
+
}
|
|
1949
|
+
targetTokenId = existingToken.id;
|
|
1950
|
+
const existingCard = await getCard(tx, existingToken.id, userId);
|
|
1951
|
+
if (!existingCard) {
|
|
1952
|
+
await ensureCard(tx, existingToken.id, userId);
|
|
1953
|
+
}
|
|
1954
|
+
linkedCount++;
|
|
1955
|
+
} else {
|
|
1956
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
1957
|
+
if (bloom < 1 || bloom > 5) {
|
|
1958
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1959
|
+
}
|
|
1960
|
+
let symbiosisMode = null;
|
|
1961
|
+
if (card.symbiosis_mode) {
|
|
1962
|
+
if (!["shadowing", "copilot", "autonomy", "none"].includes(
|
|
1963
|
+
card.symbiosis_mode
|
|
1964
|
+
)) {
|
|
1965
|
+
throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
|
|
1966
|
+
}
|
|
1967
|
+
symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
|
|
1968
|
+
}
|
|
1969
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
1970
|
+
const cleanDomain = slugify(card.domain || "");
|
|
1971
|
+
const cleanBase = slugify(baseText);
|
|
1972
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
1973
|
+
if (baseSlug.length > 60) {
|
|
1974
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
1975
|
+
}
|
|
1976
|
+
if (!baseSlug) {
|
|
1977
|
+
baseSlug = "token";
|
|
1978
|
+
}
|
|
1979
|
+
let token = await getTokenBySlug(tx, baseSlug);
|
|
1980
|
+
if (!token) {
|
|
1981
|
+
const finalSlug = await generateTokenSlug(
|
|
1982
|
+
tx,
|
|
1983
|
+
card.domain,
|
|
1984
|
+
card.concept,
|
|
1985
|
+
card.question
|
|
1986
|
+
);
|
|
1987
|
+
token = await createToken(tx, {
|
|
1988
|
+
slug: finalSlug,
|
|
1989
|
+
concept: card.concept,
|
|
1990
|
+
domain: card.domain,
|
|
1991
|
+
bloom_level: bloom,
|
|
1992
|
+
context: card.context || "",
|
|
1993
|
+
symbiosis_mode: symbiosisMode,
|
|
1994
|
+
source_link: card.source_link || originalToken.source_link || null,
|
|
1995
|
+
question: card.question || null
|
|
1996
|
+
});
|
|
1997
|
+
createdCount++;
|
|
1998
|
+
}
|
|
1999
|
+
targetTokenId = token.id;
|
|
2000
|
+
const existingCard = await getCard(tx, token.id, userId);
|
|
2001
|
+
if (!existingCard) {
|
|
2002
|
+
await ensureCard(tx, token.id, userId);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (targetTokenId === originalToken.id) {
|
|
2006
|
+
throw new Error("A token cannot be a prerequisite of itself");
|
|
2007
|
+
}
|
|
2008
|
+
await assertPrerequisiteDoesNotCreateCycle(
|
|
2009
|
+
tx,
|
|
2010
|
+
originalToken.id,
|
|
2011
|
+
targetTokenId
|
|
2012
|
+
);
|
|
2013
|
+
await tx.prepare(
|
|
2014
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
2015
|
+
).run(originalToken.id, targetTokenId);
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
return { createdCount, linkedCount };
|
|
2019
|
+
}
|
|
2020
|
+
async function confirmSourceImport(db, userId, sourceId, proposals) {
|
|
2021
|
+
let createdCount = 0;
|
|
2022
|
+
let linkedCount = 0;
|
|
2023
|
+
await db.transaction(async (tx) => {
|
|
2024
|
+
const source = await tx.prepare("SELECT id FROM sources WHERE id = ?").get(sourceId);
|
|
2025
|
+
if (!source) {
|
|
2026
|
+
throw new Error(`Source not found: ${sourceId}`);
|
|
2027
|
+
}
|
|
2028
|
+
for (const card of proposals) {
|
|
2029
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
2030
|
+
const cleanDomain = slugify(card.domain || "");
|
|
2031
|
+
const cleanBase = slugify(baseText);
|
|
2032
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
2033
|
+
if (baseSlug.length > 60) {
|
|
2034
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
2035
|
+
}
|
|
2036
|
+
if (!baseSlug) {
|
|
2037
|
+
baseSlug = "token";
|
|
2038
|
+
}
|
|
2039
|
+
let token = await getTokenBySlug(tx, baseSlug);
|
|
2040
|
+
if (!token) {
|
|
2041
|
+
const finalSlug = await generateTokenSlug(
|
|
2042
|
+
tx,
|
|
2043
|
+
card.domain,
|
|
2044
|
+
card.concept,
|
|
2045
|
+
card.question
|
|
2046
|
+
);
|
|
2047
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
2048
|
+
let symbiosisMode = null;
|
|
2049
|
+
if (card.symbiosis_mode && card.symbiosis_mode !== "none") {
|
|
2050
|
+
symbiosisMode = card.symbiosis_mode;
|
|
2051
|
+
}
|
|
2052
|
+
token = await createToken(tx, {
|
|
2053
|
+
slug: finalSlug,
|
|
2054
|
+
concept: card.concept,
|
|
2055
|
+
domain: card.domain,
|
|
2056
|
+
bloom_level: bloom,
|
|
2057
|
+
context: card.excerpt || "",
|
|
2058
|
+
symbiosis_mode: symbiosisMode,
|
|
2059
|
+
question: card.question || null
|
|
2060
|
+
});
|
|
2061
|
+
createdCount++;
|
|
2062
|
+
} else {
|
|
2063
|
+
linkedCount++;
|
|
2064
|
+
}
|
|
2065
|
+
const existingCard = await getCard(tx, token.id, userId);
|
|
2066
|
+
if (!existingCard) {
|
|
2067
|
+
await ensureCard(tx, token.id, userId);
|
|
2068
|
+
}
|
|
2069
|
+
await tx.prepare(
|
|
2070
|
+
`INSERT INTO token_sources (token_id, source_id, excerpt, page_number)
|
|
2071
|
+
VALUES (?, ?, ?, ?)
|
|
2072
|
+
ON CONFLICT(token_id, source_id) DO UPDATE SET
|
|
2073
|
+
excerpt = excluded.excerpt,
|
|
2074
|
+
page_number = excluded.page_number`
|
|
2075
|
+
).run(token.id, sourceId, card.excerpt || "", card.page_number || null);
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
return { createdCount, linkedCount };
|
|
2079
|
+
}
|
|
2080
|
+
async function assertPrerequisiteDoesNotCreateCycle(db, tokenId, prerequisiteId) {
|
|
2081
|
+
const cycleCheck = await db.prepare(
|
|
2082
|
+
`WITH RECURSIVE dependents(token_id) AS (
|
|
2083
|
+
SELECT token_id FROM prerequisites WHERE requires_id = ?
|
|
2084
|
+
UNION
|
|
2085
|
+
SELECT p.token_id FROM prerequisites p
|
|
2086
|
+
JOIN dependents d ON p.requires_id = d.token_id
|
|
2087
|
+
)
|
|
2088
|
+
SELECT COUNT(*) as n FROM dependents WHERE token_id = ?`
|
|
2089
|
+
).get(tokenId, prerequisiteId);
|
|
2090
|
+
if (cycleCheck.n > 0) {
|
|
2091
|
+
throw new Error("Cannot add prerequisite: would create a cycle");
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
1649
2094
|
|
|
1650
2095
|
// src/kernel/models/prerequisite.ts
|
|
1651
2096
|
async function buildAncestorMap(db) {
|
|
@@ -4533,22 +4978,49 @@ function getConfiguredWorkspaces(path = defaultConfigPath()) {
|
|
|
4533
4978
|
function saveConfiguredWorkspaces(workspaces, path = defaultConfigPath()) {
|
|
4534
4979
|
const config = loadInstallConfig(path);
|
|
4535
4980
|
config.workspaces = workspaces;
|
|
4981
|
+
if (config.activeWorkspaceId && !workspaces.some((workspace) => workspace.id === config.activeWorkspaceId)) {
|
|
4982
|
+
config.activeWorkspaceId = workspaces[0]?.id;
|
|
4983
|
+
}
|
|
4536
4984
|
saveInstallConfig(config, path);
|
|
4537
4985
|
}
|
|
4986
|
+
function getActiveWorkspaceId(path = defaultConfigPath()) {
|
|
4987
|
+
return loadInstallConfig(path).activeWorkspaceId;
|
|
4988
|
+
}
|
|
4989
|
+
function setActiveWorkspaceId(id, path = defaultConfigPath()) {
|
|
4990
|
+
const config = loadInstallConfig(path);
|
|
4991
|
+
if (id) {
|
|
4992
|
+
config.activeWorkspaceId = id;
|
|
4993
|
+
} else {
|
|
4994
|
+
delete config.activeWorkspaceId;
|
|
4995
|
+
}
|
|
4996
|
+
saveInstallConfig(config, path);
|
|
4997
|
+
}
|
|
4998
|
+
function getActiveWorkspace(path = defaultConfigPath()) {
|
|
4999
|
+
const config = loadInstallConfig(path);
|
|
5000
|
+
const id = config.activeWorkspaceId;
|
|
5001
|
+
return id ? config.workspaces?.find((workspace) => workspace.id === id) : void 0;
|
|
5002
|
+
}
|
|
4538
5003
|
function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
|
|
4539
|
-
const
|
|
5004
|
+
const config = loadInstallConfig(path);
|
|
5005
|
+
const current = config.workspaces ?? [];
|
|
4540
5006
|
const next = [
|
|
4541
5007
|
...current.filter((candidate) => candidate.id !== workspace.id),
|
|
4542
5008
|
workspace
|
|
4543
5009
|
];
|
|
4544
|
-
|
|
5010
|
+
config.workspaces = next;
|
|
5011
|
+
saveInstallConfig(config, path);
|
|
4545
5012
|
return next;
|
|
4546
5013
|
}
|
|
4547
5014
|
function removeConfiguredWorkspace(id, path = defaultConfigPath()) {
|
|
4548
|
-
const
|
|
5015
|
+
const config = loadInstallConfig(path);
|
|
5016
|
+
const next = (config.workspaces ?? []).filter(
|
|
4549
5017
|
(workspace) => workspace.id !== id
|
|
4550
5018
|
);
|
|
4551
|
-
|
|
5019
|
+
config.workspaces = next;
|
|
5020
|
+
if (config.activeWorkspaceId === id) {
|
|
5021
|
+
config.activeWorkspaceId = next[0]?.id;
|
|
5022
|
+
}
|
|
5023
|
+
saveInstallConfig(config, path);
|
|
4552
5024
|
return next;
|
|
4553
5025
|
}
|
|
4554
5026
|
function detectSyncProvider(dir) {
|
|
@@ -4880,7 +5352,7 @@ function getSystemProfile() {
|
|
|
4880
5352
|
import { existsSync as existsSync10 } from "fs";
|
|
4881
5353
|
import { resolve as resolve2 } from "path";
|
|
4882
5354
|
async function getRepoPaths(db) {
|
|
4883
|
-
const personalSetting = await getSetting(db, "repo.personal") ||
|
|
5355
|
+
const personalSetting = await getSetting(db, "repo.personal") || getActiveWorkspace()?.path;
|
|
4884
5356
|
const teamSetting = await getSetting(db, "repo.team");
|
|
4885
5357
|
const orgSetting = await getSetting(db, "repo.org");
|
|
4886
5358
|
return {
|
|
@@ -5067,6 +5539,9 @@ export {
|
|
|
5067
5539
|
clearReviewContextCache,
|
|
5068
5540
|
clearTursoCredentials,
|
|
5069
5541
|
compareVersions,
|
|
5542
|
+
confirmCardSplit,
|
|
5543
|
+
confirmFoundations,
|
|
5544
|
+
confirmSourceImport,
|
|
5070
5545
|
createAgentSkill,
|
|
5071
5546
|
createFSRS,
|
|
5072
5547
|
createGoal,
|
|
@@ -5099,9 +5574,12 @@ export {
|
|
|
5099
5574
|
generatePowerShellHooks,
|
|
5100
5575
|
generatePowerShellUnhooks,
|
|
5101
5576
|
generatePrompt,
|
|
5577
|
+
generateTokenSlug,
|
|
5102
5578
|
generateZshHooks,
|
|
5103
5579
|
generateZshUnhooks,
|
|
5104
5580
|
getADOCredentials,
|
|
5581
|
+
getActiveWorkspace,
|
|
5582
|
+
getActiveWorkspaceId,
|
|
5105
5583
|
getAgentSkill,
|
|
5106
5584
|
getAllSettings,
|
|
5107
5585
|
getAllSettingsDetailed,
|
|
@@ -5142,6 +5620,7 @@ export {
|
|
|
5142
5620
|
getUiObserverDir,
|
|
5143
5621
|
getUserStats,
|
|
5144
5622
|
hasCommand,
|
|
5623
|
+
importCurriculumCards,
|
|
5145
5624
|
importSnapshot,
|
|
5146
5625
|
injectShellHooks,
|
|
5147
5626
|
installFastFlowLM,
|
|
@@ -5152,6 +5631,7 @@ export {
|
|
|
5152
5631
|
isUiObservationReport,
|
|
5153
5632
|
listAgentSkills,
|
|
5154
5633
|
listGoals,
|
|
5634
|
+
listPersonalCards,
|
|
5155
5635
|
listProviderApiKeyRefs,
|
|
5156
5636
|
listTokens,
|
|
5157
5637
|
loadADOConfig,
|
|
@@ -5194,11 +5674,13 @@ export {
|
|
|
5194
5674
|
saveMachineAiConfig,
|
|
5195
5675
|
serializeGoal,
|
|
5196
5676
|
setADOCredentials,
|
|
5677
|
+
setActiveWorkspaceId,
|
|
5197
5678
|
setInstallChannel,
|
|
5198
5679
|
setInstallMode,
|
|
5199
5680
|
setProviderApiKey,
|
|
5200
5681
|
setSetting,
|
|
5201
5682
|
setTursoCredentials,
|
|
5683
|
+
slugify,
|
|
5202
5684
|
startSession,
|
|
5203
5685
|
syncObserverSidecarPolicy,
|
|
5204
5686
|
t,
|