zam-core 0.5.3 → 0.6.1
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 +1356 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +482 -19
- 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,425 @@ 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
|
+
COALESCE(
|
|
1722
|
+
t.source_link,
|
|
1723
|
+
(
|
|
1724
|
+
SELECT s.uri
|
|
1725
|
+
FROM token_sources ts
|
|
1726
|
+
INNER JOIN sources s ON s.id = ts.source_id
|
|
1727
|
+
WHERE ts.token_id = t.id
|
|
1728
|
+
ORDER BY s.created_at DESC, s.id DESC
|
|
1729
|
+
LIMIT 1
|
|
1730
|
+
)
|
|
1731
|
+
) AS sourceLink,
|
|
1732
|
+
t.question,
|
|
1733
|
+
t.created_at AS createdAt,
|
|
1734
|
+
t.updated_at AS updatedAt,
|
|
1735
|
+
c.id AS cardId,
|
|
1736
|
+
c.state,
|
|
1737
|
+
c.due_at AS dueAt,
|
|
1738
|
+
c.stability,
|
|
1739
|
+
c.difficulty,
|
|
1740
|
+
c.reps,
|
|
1741
|
+
c.lapses,
|
|
1742
|
+
c.elapsed_days AS elapsedDays,
|
|
1743
|
+
c.scheduled_days AS scheduledDays,
|
|
1744
|
+
c.blocked
|
|
1745
|
+
FROM tokens t
|
|
1746
|
+
INNER JOIN cards c ON c.token_id = t.id AND c.user_id = ?
|
|
1747
|
+
WHERE t.deprecated_at IS NULL
|
|
1748
|
+
`;
|
|
1749
|
+
const values = [userId];
|
|
1750
|
+
if (options?.domain) {
|
|
1751
|
+
sql += " AND t.domain = ?";
|
|
1752
|
+
values.push(options.domain);
|
|
1753
|
+
}
|
|
1754
|
+
if (options?.query) {
|
|
1755
|
+
const terms = options.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1756
|
+
for (const term of terms) {
|
|
1757
|
+
sql += ` AND (lower(t.slug) LIKE ? OR lower(t.concept) LIKE ? OR lower(t.domain) LIKE ? OR lower(t.question) LIKE ?)`;
|
|
1758
|
+
const pattern = `%${term}%`;
|
|
1759
|
+
values.push(pattern, pattern, pattern, pattern);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
sql += " ORDER BY t.created_at DESC";
|
|
1763
|
+
const rows = await db.prepare(sql).all(...values);
|
|
1764
|
+
return rows;
|
|
1765
|
+
}
|
|
1766
|
+
async function importCurriculumCards(db, userId, cards) {
|
|
1767
|
+
let createdCount = 0;
|
|
1768
|
+
let ensuredCount = 0;
|
|
1769
|
+
await db.transaction(async (tx) => {
|
|
1770
|
+
for (const card of cards) {
|
|
1771
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
1772
|
+
if (bloom < 1 || bloom > 5) {
|
|
1773
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1774
|
+
}
|
|
1775
|
+
let symbiosisMode = null;
|
|
1776
|
+
if (card.symbiosis_mode) {
|
|
1777
|
+
if (!["shadowing", "copilot", "autonomy", "none"].includes(
|
|
1778
|
+
card.symbiosis_mode
|
|
1779
|
+
)) {
|
|
1780
|
+
throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
|
|
1781
|
+
}
|
|
1782
|
+
symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
|
|
1783
|
+
}
|
|
1784
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
1785
|
+
const cleanDomain = slugify(card.domain || "");
|
|
1786
|
+
const cleanBase = slugify(baseText);
|
|
1787
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
1788
|
+
if (baseSlug.length > 60) {
|
|
1789
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
1790
|
+
}
|
|
1791
|
+
if (!baseSlug) {
|
|
1792
|
+
baseSlug = "token";
|
|
1793
|
+
}
|
|
1794
|
+
let token = await getTokenBySlug(tx, baseSlug);
|
|
1795
|
+
if (!token) {
|
|
1796
|
+
const finalSlug = await generateTokenSlug(
|
|
1797
|
+
tx,
|
|
1798
|
+
card.domain,
|
|
1799
|
+
card.concept,
|
|
1800
|
+
card.question
|
|
1801
|
+
);
|
|
1802
|
+
token = await createToken(tx, {
|
|
1803
|
+
slug: finalSlug,
|
|
1804
|
+
concept: card.concept,
|
|
1805
|
+
domain: card.domain,
|
|
1806
|
+
bloom_level: bloom,
|
|
1807
|
+
context: card.context || "",
|
|
1808
|
+
symbiosis_mode: symbiosisMode,
|
|
1809
|
+
source_link: card.source_link || null,
|
|
1810
|
+
question: card.question || null
|
|
1811
|
+
});
|
|
1812
|
+
createdCount++;
|
|
1813
|
+
}
|
|
1814
|
+
const existingCard = await getCard(tx, token.id, userId);
|
|
1815
|
+
if (!existingCard) {
|
|
1816
|
+
await ensureCard(tx, token.id, userId);
|
|
1817
|
+
ensuredCount++;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
return { createdCount, ensuredCount };
|
|
1822
|
+
}
|
|
1823
|
+
async function confirmCardSplit(db, userId, originalSlug, action, originalQuestion, originalConcept, proposals) {
|
|
1824
|
+
if (action !== "block" && action !== "remove") {
|
|
1825
|
+
throw new Error(`Invalid split action: ${action}`);
|
|
1826
|
+
}
|
|
1827
|
+
if (proposals.length < 2 || proposals.length > 4) {
|
|
1828
|
+
throw new Error("A card split requires between 2 and 4 proposals");
|
|
1829
|
+
}
|
|
1830
|
+
const originalToken = await getTokenBySlug(db, originalSlug);
|
|
1831
|
+
if (!originalToken) {
|
|
1832
|
+
throw new Error(`Original token not found: ${originalSlug}`);
|
|
1833
|
+
}
|
|
1834
|
+
let createdCount = 0;
|
|
1835
|
+
let ensuredCount = 0;
|
|
1836
|
+
await db.transaction(async (tx) => {
|
|
1837
|
+
const originalCard = await getCard(tx, originalToken.id, userId);
|
|
1838
|
+
if (!originalCard) {
|
|
1839
|
+
throw new Error(
|
|
1840
|
+
`Card not found for token ${originalSlug} and user ${userId}`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
const proposalTokens = [];
|
|
1844
|
+
for (const card of proposals) {
|
|
1845
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
1846
|
+
if (bloom < 1 || bloom > 5) {
|
|
1847
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1848
|
+
}
|
|
1849
|
+
let symbiosisMode = null;
|
|
1850
|
+
if (card.symbiosis_mode) {
|
|
1851
|
+
if (!["shadowing", "copilot", "autonomy", "none"].includes(
|
|
1852
|
+
card.symbiosis_mode
|
|
1853
|
+
)) {
|
|
1854
|
+
throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
|
|
1855
|
+
}
|
|
1856
|
+
symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
|
|
1857
|
+
}
|
|
1858
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
1859
|
+
const cleanDomain = slugify(card.domain || "");
|
|
1860
|
+
const cleanBase = slugify(baseText);
|
|
1861
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
1862
|
+
if (baseSlug.length > 60) {
|
|
1863
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
1864
|
+
}
|
|
1865
|
+
if (!baseSlug) {
|
|
1866
|
+
baseSlug = "token";
|
|
1867
|
+
}
|
|
1868
|
+
let token = await getTokenBySlug(tx, baseSlug);
|
|
1869
|
+
if (!token) {
|
|
1870
|
+
const finalSlug = await generateTokenSlug(
|
|
1871
|
+
tx,
|
|
1872
|
+
card.domain,
|
|
1873
|
+
card.concept,
|
|
1874
|
+
card.question
|
|
1875
|
+
);
|
|
1876
|
+
token = await createToken(tx, {
|
|
1877
|
+
slug: finalSlug,
|
|
1878
|
+
concept: card.concept,
|
|
1879
|
+
domain: card.domain,
|
|
1880
|
+
bloom_level: bloom,
|
|
1881
|
+
context: card.context || "",
|
|
1882
|
+
symbiosis_mode: symbiosisMode,
|
|
1883
|
+
source_link: card.source_link || originalToken.source_link || null,
|
|
1884
|
+
question: card.question || null
|
|
1885
|
+
});
|
|
1886
|
+
createdCount++;
|
|
1887
|
+
}
|
|
1888
|
+
if (token.id === originalToken.id) {
|
|
1889
|
+
throw new Error("A token cannot be a prerequisite of itself");
|
|
1890
|
+
}
|
|
1891
|
+
await assertPrerequisiteDoesNotCreateCycle(
|
|
1892
|
+
tx,
|
|
1893
|
+
originalToken.id,
|
|
1894
|
+
token.id
|
|
1895
|
+
);
|
|
1896
|
+
proposalTokens.push(token);
|
|
1897
|
+
const existingCard = await getCard(tx, token.id, userId);
|
|
1898
|
+
if (!existingCard) {
|
|
1899
|
+
await ensureCard(tx, token.id, userId);
|
|
1900
|
+
ensuredCount++;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
if (action === "block") {
|
|
1904
|
+
await tx.prepare(
|
|
1905
|
+
"UPDATE tokens SET question = ?, concept = ?, updated_at = ? WHERE id = ?"
|
|
1906
|
+
).run(
|
|
1907
|
+
originalQuestion || null,
|
|
1908
|
+
originalConcept,
|
|
1909
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1910
|
+
originalToken.id
|
|
1911
|
+
);
|
|
1912
|
+
for (const propToken of proposalTokens) {
|
|
1913
|
+
await tx.prepare(
|
|
1914
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
1915
|
+
).run(originalToken.id, propToken.id);
|
|
1916
|
+
}
|
|
1917
|
+
await tx.prepare(
|
|
1918
|
+
"UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
|
|
1919
|
+
).run(originalToken.id, userId);
|
|
1920
|
+
for (const propToken of proposalTokens) {
|
|
1921
|
+
const card = await ensureCard(tx, propToken.id, userId);
|
|
1922
|
+
if (card.blocked === 1) {
|
|
1923
|
+
const prereqOfPrereq = await tx.prepare(
|
|
1924
|
+
"SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
|
|
1925
|
+
).get(propToken.id);
|
|
1926
|
+
if (prereqOfPrereq.n === 0) {
|
|
1927
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1928
|
+
await tx.prepare("UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?").run(now, card.id);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
} else if (action === "remove") {
|
|
1933
|
+
await deleteCardForUser(tx, originalToken.id, userId);
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
return { createdCount, ensuredCount };
|
|
1937
|
+
}
|
|
1938
|
+
async function confirmFoundations(db, userId, originalSlug, proposals) {
|
|
1939
|
+
const originalToken = await getTokenBySlug(db, originalSlug);
|
|
1940
|
+
if (!originalToken) {
|
|
1941
|
+
throw new Error(`Original token not found: ${originalSlug}`);
|
|
1942
|
+
}
|
|
1943
|
+
let createdCount = 0;
|
|
1944
|
+
let linkedCount = 0;
|
|
1945
|
+
await db.transaction(async (tx) => {
|
|
1946
|
+
const originalCard = await getCard(tx, originalToken.id, userId);
|
|
1947
|
+
if (!originalCard) {
|
|
1948
|
+
throw new Error(
|
|
1949
|
+
`Card not found for token ${originalSlug} and user ${userId}`
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
for (const card of proposals) {
|
|
1953
|
+
let targetTokenId;
|
|
1954
|
+
if (card.exists && card.slug) {
|
|
1955
|
+
const existingToken = await getTokenBySlug(tx, card.slug);
|
|
1956
|
+
if (!existingToken) {
|
|
1957
|
+
throw new Error(`Prerequisite token not found by slug: ${card.slug}`);
|
|
1958
|
+
}
|
|
1959
|
+
targetTokenId = existingToken.id;
|
|
1960
|
+
const existingCard = await getCard(tx, existingToken.id, userId);
|
|
1961
|
+
if (!existingCard) {
|
|
1962
|
+
await ensureCard(tx, existingToken.id, userId);
|
|
1963
|
+
}
|
|
1964
|
+
linkedCount++;
|
|
1965
|
+
} else {
|
|
1966
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
1967
|
+
if (bloom < 1 || bloom > 5) {
|
|
1968
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1969
|
+
}
|
|
1970
|
+
let symbiosisMode = null;
|
|
1971
|
+
if (card.symbiosis_mode) {
|
|
1972
|
+
if (!["shadowing", "copilot", "autonomy", "none"].includes(
|
|
1973
|
+
card.symbiosis_mode
|
|
1974
|
+
)) {
|
|
1975
|
+
throw new Error(`Invalid symbiosis_mode: ${card.symbiosis_mode}`);
|
|
1976
|
+
}
|
|
1977
|
+
symbiosisMode = card.symbiosis_mode === "none" ? null : card.symbiosis_mode;
|
|
1978
|
+
}
|
|
1979
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
1980
|
+
const cleanDomain = slugify(card.domain || "");
|
|
1981
|
+
const cleanBase = slugify(baseText);
|
|
1982
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
1983
|
+
if (baseSlug.length > 60) {
|
|
1984
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
1985
|
+
}
|
|
1986
|
+
if (!baseSlug) {
|
|
1987
|
+
baseSlug = "token";
|
|
1988
|
+
}
|
|
1989
|
+
let token = await getTokenBySlug(tx, baseSlug);
|
|
1990
|
+
if (!token) {
|
|
1991
|
+
const finalSlug = await generateTokenSlug(
|
|
1992
|
+
tx,
|
|
1993
|
+
card.domain,
|
|
1994
|
+
card.concept,
|
|
1995
|
+
card.question
|
|
1996
|
+
);
|
|
1997
|
+
token = await createToken(tx, {
|
|
1998
|
+
slug: finalSlug,
|
|
1999
|
+
concept: card.concept,
|
|
2000
|
+
domain: card.domain,
|
|
2001
|
+
bloom_level: bloom,
|
|
2002
|
+
context: card.context || "",
|
|
2003
|
+
symbiosis_mode: symbiosisMode,
|
|
2004
|
+
source_link: card.source_link || originalToken.source_link || null,
|
|
2005
|
+
question: card.question || null
|
|
2006
|
+
});
|
|
2007
|
+
createdCount++;
|
|
2008
|
+
}
|
|
2009
|
+
targetTokenId = token.id;
|
|
2010
|
+
const existingCard = await getCard(tx, token.id, userId);
|
|
2011
|
+
if (!existingCard) {
|
|
2012
|
+
await ensureCard(tx, token.id, userId);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (targetTokenId === originalToken.id) {
|
|
2016
|
+
throw new Error("A token cannot be a prerequisite of itself");
|
|
2017
|
+
}
|
|
2018
|
+
await assertPrerequisiteDoesNotCreateCycle(
|
|
2019
|
+
tx,
|
|
2020
|
+
originalToken.id,
|
|
2021
|
+
targetTokenId
|
|
2022
|
+
);
|
|
2023
|
+
await tx.prepare(
|
|
2024
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
2025
|
+
).run(originalToken.id, targetTokenId);
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
return { createdCount, linkedCount };
|
|
2029
|
+
}
|
|
2030
|
+
async function confirmSourceImport(db, userId, sourceId, proposals) {
|
|
2031
|
+
let createdCount = 0;
|
|
2032
|
+
let linkedCount = 0;
|
|
2033
|
+
await db.transaction(async (tx) => {
|
|
2034
|
+
const source = await tx.prepare("SELECT id FROM sources WHERE id = ?").get(sourceId);
|
|
2035
|
+
if (!source) {
|
|
2036
|
+
throw new Error(`Source not found: ${sourceId}`);
|
|
2037
|
+
}
|
|
2038
|
+
for (const card of proposals) {
|
|
2039
|
+
const baseText = card.question && card.question.trim().length > 0 ? card.question : card.concept;
|
|
2040
|
+
const cleanDomain = slugify(card.domain || "");
|
|
2041
|
+
const cleanBase = slugify(baseText);
|
|
2042
|
+
let baseSlug = cleanDomain ? `${cleanDomain}-${cleanBase}` : cleanBase;
|
|
2043
|
+
if (baseSlug.length > 60) {
|
|
2044
|
+
baseSlug = baseSlug.slice(0, 60).replace(/-$/, "");
|
|
2045
|
+
}
|
|
2046
|
+
if (!baseSlug) {
|
|
2047
|
+
baseSlug = "token";
|
|
2048
|
+
}
|
|
2049
|
+
let token = await getTokenBySlug(tx, baseSlug);
|
|
2050
|
+
if (!token) {
|
|
2051
|
+
const finalSlug = await generateTokenSlug(
|
|
2052
|
+
tx,
|
|
2053
|
+
card.domain,
|
|
2054
|
+
card.concept,
|
|
2055
|
+
card.question
|
|
2056
|
+
);
|
|
2057
|
+
const bloom = card.bloom_level !== void 0 ? card.bloom_level : 1;
|
|
2058
|
+
let symbiosisMode = null;
|
|
2059
|
+
if (card.symbiosis_mode && card.symbiosis_mode !== "none") {
|
|
2060
|
+
symbiosisMode = card.symbiosis_mode;
|
|
2061
|
+
}
|
|
2062
|
+
token = await createToken(tx, {
|
|
2063
|
+
slug: finalSlug,
|
|
2064
|
+
concept: card.concept,
|
|
2065
|
+
domain: card.domain,
|
|
2066
|
+
bloom_level: bloom,
|
|
2067
|
+
context: card.excerpt || "",
|
|
2068
|
+
symbiosis_mode: symbiosisMode,
|
|
2069
|
+
question: card.question || null
|
|
2070
|
+
});
|
|
2071
|
+
createdCount++;
|
|
2072
|
+
} else {
|
|
2073
|
+
linkedCount++;
|
|
2074
|
+
}
|
|
2075
|
+
const existingCard = await getCard(tx, token.id, userId);
|
|
2076
|
+
if (!existingCard) {
|
|
2077
|
+
await ensureCard(tx, token.id, userId);
|
|
2078
|
+
}
|
|
2079
|
+
await tx.prepare(
|
|
2080
|
+
`INSERT INTO token_sources (token_id, source_id, excerpt, page_number)
|
|
2081
|
+
VALUES (?, ?, ?, ?)
|
|
2082
|
+
ON CONFLICT(token_id, source_id) DO UPDATE SET
|
|
2083
|
+
excerpt = excluded.excerpt,
|
|
2084
|
+
page_number = excluded.page_number`
|
|
2085
|
+
).run(token.id, sourceId, card.excerpt || "", card.page_number || null);
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
return { createdCount, linkedCount };
|
|
2089
|
+
}
|
|
2090
|
+
async function assertPrerequisiteDoesNotCreateCycle(db, tokenId, prerequisiteId) {
|
|
2091
|
+
const cycleCheck = await db.prepare(
|
|
2092
|
+
`WITH RECURSIVE dependents(token_id) AS (
|
|
2093
|
+
SELECT token_id FROM prerequisites WHERE requires_id = ?
|
|
2094
|
+
UNION
|
|
2095
|
+
SELECT p.token_id FROM prerequisites p
|
|
2096
|
+
JOIN dependents d ON p.requires_id = d.token_id
|
|
2097
|
+
)
|
|
2098
|
+
SELECT COUNT(*) as n FROM dependents WHERE token_id = ?`
|
|
2099
|
+
).get(tokenId, prerequisiteId);
|
|
2100
|
+
if (cycleCheck.n > 0) {
|
|
2101
|
+
throw new Error("Cannot add prerequisite: would create a cycle");
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
1649
2104
|
|
|
1650
2105
|
// src/kernel/models/prerequisite.ts
|
|
1651
2106
|
async function buildAncestorMap(db) {
|
|
@@ -4637,12 +5092,7 @@ function installFastFlowLM() {
|
|
|
4637
5092
|
}
|
|
4638
5093
|
}
|
|
4639
5094
|
function installOllama() {
|
|
4640
|
-
|
|
4641
|
-
const isWin = process.platform === "win32";
|
|
4642
|
-
const hasOllama = hasCommand("ollama") || isMac && existsSync9("/Applications/Ollama.app") || isWin && existsSync9(
|
|
4643
|
-
join10(homedir7(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
|
|
4644
|
-
);
|
|
4645
|
-
if (hasOllama) {
|
|
5095
|
+
if (isOllamaInstalled()) {
|
|
4646
5096
|
return { success: true, message: "Ollama is already installed." };
|
|
4647
5097
|
}
|
|
4648
5098
|
if (process.platform === "darwin") {
|
|
@@ -4697,19 +5147,23 @@ function installOllama() {
|
|
|
4697
5147
|
}
|
|
4698
5148
|
}
|
|
4699
5149
|
}
|
|
4700
|
-
function resolveOllamaCommand() {
|
|
4701
|
-
|
|
4702
|
-
const
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
5150
|
+
function resolveOllamaCommand(options = {}) {
|
|
5151
|
+
const platform = options.platform ?? process.platform;
|
|
5152
|
+
const homeDir = options.homeDir ?? homedir7();
|
|
5153
|
+
const commandAvailable = options.commandAvailable ?? hasCommand;
|
|
5154
|
+
const pathExists = options.pathExists ?? existsSync9;
|
|
5155
|
+
if (commandAvailable("ollama")) return "ollama";
|
|
5156
|
+
const candidates = platform === "win32" ? [join10(homeDir, "AppData", "Local", "Programs", "Ollama", "ollama.exe")] : platform === "darwin" ? [
|
|
5157
|
+
"/opt/homebrew/bin/ollama",
|
|
5158
|
+
"/usr/local/bin/ollama",
|
|
5159
|
+
"/Applications/Ollama.app/Contents/Resources/ollama"
|
|
5160
|
+
] : ["/usr/local/bin/ollama", "/usr/bin/ollama"];
|
|
5161
|
+
return candidates.find((candidate) => pathExists(candidate));
|
|
5162
|
+
}
|
|
5163
|
+
function isOllamaInstalled(options = {}) {
|
|
5164
|
+
const platform = options.platform ?? process.platform;
|
|
5165
|
+
const pathExists = options.pathExists ?? existsSync9;
|
|
5166
|
+
return resolveOllamaCommand(options) !== void 0 || platform === "darwin" && pathExists("/Applications/Ollama.app");
|
|
4713
5167
|
}
|
|
4714
5168
|
function prepareLocalModel(runner, model) {
|
|
4715
5169
|
if (runner === "fastflowlm") {
|
|
@@ -5094,6 +5548,9 @@ export {
|
|
|
5094
5548
|
clearReviewContextCache,
|
|
5095
5549
|
clearTursoCredentials,
|
|
5096
5550
|
compareVersions,
|
|
5551
|
+
confirmCardSplit,
|
|
5552
|
+
confirmFoundations,
|
|
5553
|
+
confirmSourceImport,
|
|
5097
5554
|
createAgentSkill,
|
|
5098
5555
|
createFSRS,
|
|
5099
5556
|
createGoal,
|
|
@@ -5126,6 +5583,7 @@ export {
|
|
|
5126
5583
|
generatePowerShellHooks,
|
|
5127
5584
|
generatePowerShellUnhooks,
|
|
5128
5585
|
generatePrompt,
|
|
5586
|
+
generateTokenSlug,
|
|
5129
5587
|
generateZshHooks,
|
|
5130
5588
|
generateZshUnhooks,
|
|
5131
5589
|
getADOCredentials,
|
|
@@ -5171,6 +5629,7 @@ export {
|
|
|
5171
5629
|
getUiObserverDir,
|
|
5172
5630
|
getUserStats,
|
|
5173
5631
|
hasCommand,
|
|
5632
|
+
importCurriculumCards,
|
|
5174
5633
|
importSnapshot,
|
|
5175
5634
|
injectShellHooks,
|
|
5176
5635
|
installFastFlowLM,
|
|
@@ -5178,9 +5637,11 @@ export {
|
|
|
5178
5637
|
installOpenCode,
|
|
5179
5638
|
interleave,
|
|
5180
5639
|
isObserverPolicyConfigured,
|
|
5640
|
+
isOllamaInstalled,
|
|
5181
5641
|
isUiObservationReport,
|
|
5182
5642
|
listAgentSkills,
|
|
5183
5643
|
listGoals,
|
|
5644
|
+
listPersonalCards,
|
|
5184
5645
|
listProviderApiKeyRefs,
|
|
5185
5646
|
listTokens,
|
|
5186
5647
|
loadADOConfig,
|
|
@@ -5214,6 +5675,7 @@ export {
|
|
|
5214
5675
|
resolveAllBeliefPaths,
|
|
5215
5676
|
resolveAllGoalPaths,
|
|
5216
5677
|
resolveObserverPolicy,
|
|
5678
|
+
resolveOllamaCommand,
|
|
5217
5679
|
resolveReference,
|
|
5218
5680
|
resolveRepoPath,
|
|
5219
5681
|
resolveReviewContext,
|
|
@@ -5229,6 +5691,7 @@ export {
|
|
|
5229
5691
|
setProviderApiKey,
|
|
5230
5692
|
setSetting,
|
|
5231
5693
|
setTursoCredentials,
|
|
5694
|
+
slugify,
|
|
5232
5695
|
startSession,
|
|
5233
5696
|
syncObserverSidecarPolicy,
|
|
5234
5697
|
t,
|