@systemdox/mcp-server 0.1.51 → 0.1.53
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.js +131 -19
- package/dist/http.js +121 -17
- package/dist/index.js +121 -17
- package/dist/lib/context-builder.js +98 -11
- package/dist/lib/scoring.d.ts +5 -0
- package/dist/lib/spec-label.d.ts +5 -0
- package/dist/lib/team-setup-types.d.ts +1 -1
- package/dist/server.js +121 -17
- package/dist/tools/get-docs.js +14 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -870,6 +870,17 @@ ${text3}`
|
|
|
870
870
|
|
|
871
871
|
// src/tools/get-specs.ts
|
|
872
872
|
import { z as z2 } from "zod";
|
|
873
|
+
|
|
874
|
+
// src/lib/spec-label.ts
|
|
875
|
+
var DECISION_PREFIX = "ADR: ";
|
|
876
|
+
function isDecisionDraft(name) {
|
|
877
|
+
return name.startsWith(DECISION_PREFIX);
|
|
878
|
+
}
|
|
879
|
+
function specLabel(spec) {
|
|
880
|
+
return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/tools/get-specs.ts
|
|
873
884
|
function registerGetSpecs(server, apiClient2) {
|
|
874
885
|
server.registerTool(
|
|
875
886
|
"get_specs",
|
|
@@ -934,7 +945,7 @@ ${spec.taskContent}`);
|
|
|
934
945
|
};
|
|
935
946
|
}
|
|
936
947
|
const text3 = filtered.map(
|
|
937
|
-
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s
|
|
948
|
+
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${specLabel(s)}` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
938
949
|
).join("\n");
|
|
939
950
|
return {
|
|
940
951
|
content: [
|
|
@@ -952,6 +963,11 @@ ${text3}`
|
|
|
952
963
|
|
|
953
964
|
// src/tools/get-docs.ts
|
|
954
965
|
import { z as z3 } from "zod";
|
|
966
|
+
var PATH_FILTER_LIMIT = "1000";
|
|
967
|
+
function literalPrefix(pattern) {
|
|
968
|
+
const wildcard = pattern.indexOf("*");
|
|
969
|
+
return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
|
|
970
|
+
}
|
|
955
971
|
function matchGlob(path, pattern) {
|
|
956
972
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
957
973
|
return new RegExp(`^${regex}$`).test(path);
|
|
@@ -1012,11 +1028,17 @@ ${doc.content}`;
|
|
|
1012
1028
|
const params = {};
|
|
1013
1029
|
if (repo) params.repo = repo;
|
|
1014
1030
|
if (search) params.q = search;
|
|
1031
|
+
if (path) {
|
|
1032
|
+
params.limit = PATH_FILTER_LIMIT;
|
|
1033
|
+
const prefix = literalPrefix(path);
|
|
1034
|
+
if (prefix) params.pathPrefix = prefix;
|
|
1035
|
+
}
|
|
1015
1036
|
const response = await apiClient2.get(
|
|
1016
1037
|
"/v1/docs",
|
|
1017
1038
|
params
|
|
1018
1039
|
);
|
|
1019
1040
|
let items = response.items;
|
|
1041
|
+
const unread = path ? response.total - response.items.length : 0;
|
|
1020
1042
|
if (path) {
|
|
1021
1043
|
items = items.filter((item) => matchGlob(item.path, path));
|
|
1022
1044
|
}
|
|
@@ -1046,7 +1068,9 @@ ${doc.content}`;
|
|
|
1046
1068
|
|
|
1047
1069
|
${text3}
|
|
1048
1070
|
|
|
1049
|
-
|
|
1071
|
+
` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
|
|
1072
|
+
|
|
1073
|
+
` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
|
|
1050
1074
|
}
|
|
1051
1075
|
]
|
|
1052
1076
|
};
|
|
@@ -1147,7 +1171,9 @@ ${deps}`);
|
|
|
1147
1171
|
}
|
|
1148
1172
|
if (ctx.adrs.length > 0) {
|
|
1149
1173
|
const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
|
|
1150
|
-
|
|
1174
|
+
const total = ctx.adrTotal ?? ctx.adrs.length;
|
|
1175
|
+
const count = total > ctx.adrs.length ? `${ctx.adrs.length} newest of ${total}` : `${ctx.adrs.length}`;
|
|
1176
|
+
sections.push(`## Architecture Decision Records (${count})
|
|
1151
1177
|
|
|
1152
1178
|
${adrs}`);
|
|
1153
1179
|
}
|
|
@@ -1519,6 +1545,9 @@ var SEVERITY_SCORES = {
|
|
|
1519
1545
|
medium: 0.5,
|
|
1520
1546
|
low: 0.25
|
|
1521
1547
|
};
|
|
1548
|
+
var MIN_TITLE_SHARE = 1 / 3;
|
|
1549
|
+
var RECORD_PATH = /(^|\/)changelog\//;
|
|
1550
|
+
var RECORD_FACTOR = 0.5;
|
|
1522
1551
|
var CHARS_PER_TOKEN = 4;
|
|
1523
1552
|
function estimateTokens(text3) {
|
|
1524
1553
|
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
@@ -1571,6 +1600,32 @@ function scoreSeverity(item) {
|
|
|
1571
1600
|
if (!item.severity) return 0.5;
|
|
1572
1601
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
1573
1602
|
}
|
|
1603
|
+
function stem(word) {
|
|
1604
|
+
return word.length > 3 ? word.replace(/s$/, "") : word;
|
|
1605
|
+
}
|
|
1606
|
+
function stems(text3) {
|
|
1607
|
+
return new Set(
|
|
1608
|
+
text3.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
function titleShare(title, keywords) {
|
|
1612
|
+
const wanted = stems(keywords.join(" "));
|
|
1613
|
+
if (wanted.size === 0) return 0;
|
|
1614
|
+
const held = stems(title);
|
|
1615
|
+
let matches = 0;
|
|
1616
|
+
for (const word of wanted) {
|
|
1617
|
+
if (held.has(word)) matches++;
|
|
1618
|
+
}
|
|
1619
|
+
return matches / wanted.size;
|
|
1620
|
+
}
|
|
1621
|
+
function scoreReference(item, keywords) {
|
|
1622
|
+
const share = titleShare(item.title, keywords);
|
|
1623
|
+
const score = Math.max(
|
|
1624
|
+
item.relevance ?? 0,
|
|
1625
|
+
share >= MIN_TITLE_SHARE ? share : 0
|
|
1626
|
+
);
|
|
1627
|
+
return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
|
|
1628
|
+
}
|
|
1574
1629
|
function extractKeywords(text3) {
|
|
1575
1630
|
const stopWords = /* @__PURE__ */ new Set([
|
|
1576
1631
|
"the",
|
|
@@ -1667,7 +1722,8 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
1667
1722
|
const keywordScore = scoreKeywordMatch(item, keywords);
|
|
1668
1723
|
const recencyScore = scoreRecency(item);
|
|
1669
1724
|
const severityScore = scoreSeverity(item);
|
|
1670
|
-
const
|
|
1725
|
+
const isReference = item.type === "spec" || item.type === "doc";
|
|
1726
|
+
const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
1671
1727
|
return {
|
|
1672
1728
|
type: item.type,
|
|
1673
1729
|
id: item.id,
|
|
@@ -1676,7 +1732,7 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
1676
1732
|
score,
|
|
1677
1733
|
tokenEstimate: estimateTokens(item.content)
|
|
1678
1734
|
};
|
|
1679
|
-
}).sort((a, b) => b.score - a.score);
|
|
1735
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
|
1680
1736
|
}
|
|
1681
1737
|
function selectWithinBudget(items, maxTokens) {
|
|
1682
1738
|
const selected = [];
|
|
@@ -1698,6 +1754,10 @@ var MAX_HYDRATED = 8;
|
|
|
1698
1754
|
var MAX_TAIL = 10;
|
|
1699
1755
|
var TAIL_SHARE = 0.15;
|
|
1700
1756
|
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
1757
|
+
var STACK_SHARE = 0.1;
|
|
1758
|
+
var MIN_REFERENCES = 3;
|
|
1759
|
+
var SEARCH_RESULTS = 30;
|
|
1760
|
+
var MAX_QUERY_CHARS = 300;
|
|
1701
1761
|
function formatGuardrail(g) {
|
|
1702
1762
|
let text3 = `## ${g.title} [${g.severity}]
|
|
1703
1763
|
|
|
@@ -1716,7 +1776,7 @@ ${g.fix}
|
|
|
1716
1776
|
}
|
|
1717
1777
|
function formatSpecHeadline(s) {
|
|
1718
1778
|
return `### ${s.name}
|
|
1719
|
-
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s
|
|
1779
|
+
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
|
|
1720
1780
|
}
|
|
1721
1781
|
function formatDocHeadline(d) {
|
|
1722
1782
|
return `### ${d.title}
|
|
@@ -1727,6 +1787,21 @@ function formatStack(entries2) {
|
|
|
1727
1787
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
1728
1788
|
).join("\n");
|
|
1729
1789
|
}
|
|
1790
|
+
function selectStack(entries2, words, maxTokens) {
|
|
1791
|
+
const named = (e) => {
|
|
1792
|
+
const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
|
|
1793
|
+
return words.some((w) => entry.includes(w));
|
|
1794
|
+
};
|
|
1795
|
+
const ordered = [...entries2.filter(named), ...entries2.filter((e) => !named(e))];
|
|
1796
|
+
const shown = [];
|
|
1797
|
+
for (const entry of ordered) {
|
|
1798
|
+
if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
shown.push(entry);
|
|
1802
|
+
}
|
|
1803
|
+
return { text: formatStack(shown), hidden: entries2.length - shown.length };
|
|
1804
|
+
}
|
|
1730
1805
|
function formatRules(rules) {
|
|
1731
1806
|
const byCategory = /* @__PURE__ */ new Map();
|
|
1732
1807
|
for (const r of rules) {
|
|
@@ -1748,6 +1823,21 @@ function formatRules(rules) {
|
|
|
1748
1823
|
|
|
1749
1824
|
${text3.trim()}`;
|
|
1750
1825
|
}
|
|
1826
|
+
async function findDocs(api, taskDescription) {
|
|
1827
|
+
const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
|
|
1828
|
+
if (query) {
|
|
1829
|
+
try {
|
|
1830
|
+
const found = await api.get("/v1/docs", {
|
|
1831
|
+
q: query,
|
|
1832
|
+
limit: String(SEARCH_RESULTS)
|
|
1833
|
+
});
|
|
1834
|
+
if (found.items.length > 0) return { ...found, searched: true };
|
|
1835
|
+
} catch {
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const listed = await api.get("/v1/docs");
|
|
1839
|
+
return { ...listed, searched: false };
|
|
1840
|
+
}
|
|
1751
1841
|
async function fetchSpecBody(api, draftId) {
|
|
1752
1842
|
const spec = await api.get(
|
|
1753
1843
|
`/v1/specs/${encodeURIComponent(draftId)}`
|
|
@@ -1848,7 +1938,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1848
1938
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
1849
1939
|
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
1850
1940
|
api.get("/v1/specs").catch(orEmpty([])),
|
|
1851
|
-
api
|
|
1941
|
+
findDocs(api, taskDescription).catch(
|
|
1942
|
+
orEmpty({ items: [], searched: false })
|
|
1943
|
+
),
|
|
1852
1944
|
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
1853
1945
|
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
1854
1946
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
@@ -1861,6 +1953,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1861
1953
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
1862
1954
|
const refs = /* @__PURE__ */ new Map();
|
|
1863
1955
|
const key = (type, id) => `${type}:${id}`;
|
|
1956
|
+
const docTitles = new Set(docs.items.map((d) => d.title));
|
|
1957
|
+
const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
|
|
1864
1958
|
const scorable = [
|
|
1865
1959
|
...guardrails.map((g) => ({
|
|
1866
1960
|
type: "guardrail",
|
|
@@ -1871,12 +1965,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1871
1965
|
severity: g.severity,
|
|
1872
1966
|
updatedAt: g.updatedAt
|
|
1873
1967
|
})),
|
|
1874
|
-
...specs.filter((s) => s.status !== "archived").map((s) => {
|
|
1968
|
+
...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
|
|
1875
1969
|
refs.set(key("spec", s.draftId), {
|
|
1876
1970
|
type: "spec",
|
|
1877
1971
|
id: s.draftId,
|
|
1878
1972
|
title: s.name,
|
|
1879
|
-
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s
|
|
1973
|
+
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
|
|
1880
1974
|
fetchHint: `get_specs(draft_id="${s.draftId}")`
|
|
1881
1975
|
});
|
|
1882
1976
|
return {
|
|
@@ -1887,7 +1981,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1887
1981
|
updatedAt: s.updatedAt
|
|
1888
1982
|
};
|
|
1889
1983
|
}),
|
|
1890
|
-
...docs.items.map((d) => {
|
|
1984
|
+
...docs.items.map((d, position) => {
|
|
1891
1985
|
const section = selectSection(d.outline, taskDescription);
|
|
1892
1986
|
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
1893
1987
|
refs.set(key("doc", d.id), {
|
|
@@ -1906,7 +2000,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1906
2000
|
title: d.title,
|
|
1907
2001
|
content: formatDocHeadline(d),
|
|
1908
2002
|
category: d.doc_type === "adr" ? "architecture" : void 0,
|
|
1909
|
-
updatedAt: d.synced_at
|
|
2003
|
+
updatedAt: d.synced_at,
|
|
2004
|
+
path: d.path,
|
|
2005
|
+
relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
|
|
1910
2006
|
};
|
|
1911
2007
|
})
|
|
1912
2008
|
];
|
|
@@ -1914,9 +2010,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1914
2010
|
const sections = [];
|
|
1915
2011
|
let overhead = 0;
|
|
1916
2012
|
if (stack.length > 0) {
|
|
2013
|
+
const { text: text3, hidden } = selectStack(
|
|
2014
|
+
stack,
|
|
2015
|
+
[...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
|
|
2016
|
+
Math.floor(budget * STACK_SHARE)
|
|
2017
|
+
);
|
|
1917
2018
|
const section = `## Tech Stack
|
|
1918
2019
|
|
|
1919
|
-
${
|
|
2020
|
+
${text3}` + (hidden > 0 ? `
|
|
2021
|
+
|
|
2022
|
+
_${hidden} more with \`get_tech_stack\`._` : "");
|
|
1920
2023
|
sections.push(section);
|
|
1921
2024
|
overhead += estimateTokens(section);
|
|
1922
2025
|
}
|
|
@@ -2035,9 +2138,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
|
|
|
2035
2138
|
const tailLines = [];
|
|
2036
2139
|
for (const meta of tailCandidates) {
|
|
2037
2140
|
const cost = estimateTokens(meta.listLine);
|
|
2038
|
-
|
|
2141
|
+
const named = blocks.length + tailLines.length;
|
|
2142
|
+
if (cost > remaining && named >= MIN_REFERENCES) break;
|
|
2039
2143
|
tailLines.push(meta.listLine);
|
|
2040
|
-
remaining
|
|
2144
|
+
remaining = Math.max(0, remaining - cost);
|
|
2041
2145
|
}
|
|
2042
2146
|
if (tailLines.length > 0) {
|
|
2043
2147
|
sections.push(
|
|
@@ -3020,9 +3124,9 @@ function localDate(now) {
|
|
|
3020
3124
|
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
3021
3125
|
}
|
|
3022
3126
|
function conflictCopyName(path, date, taken) {
|
|
3023
|
-
const
|
|
3127
|
+
const stem2 = path.replace(/\.md$/, "");
|
|
3024
3128
|
for (let n = 1; ; n++) {
|
|
3025
|
-
const name = `${
|
|
3129
|
+
const name = `${stem2}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
|
|
3026
3130
|
if (!taken.has(name.toLowerCase())) return name;
|
|
3027
3131
|
}
|
|
3028
3132
|
}
|
|
@@ -3909,7 +4013,7 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
|
3909
4013
|
}
|
|
3910
4014
|
|
|
3911
4015
|
// src/server.ts
|
|
3912
|
-
var SERVER_VERSION = true ? "0.1.
|
|
4016
|
+
var SERVER_VERSION = true ? "0.1.53" : "0.0.0-dev";
|
|
3913
4017
|
function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
|
|
3914
4018
|
const server = new McpServer(
|
|
3915
4019
|
{
|
|
@@ -82,6 +82,9 @@ var SEVERITY_SCORES = {
|
|
|
82
82
|
medium: 0.5,
|
|
83
83
|
low: 0.25
|
|
84
84
|
};
|
|
85
|
+
var MIN_TITLE_SHARE = 1 / 3;
|
|
86
|
+
var RECORD_PATH = /(^|\/)changelog\//;
|
|
87
|
+
var RECORD_FACTOR = 0.5;
|
|
85
88
|
var CHARS_PER_TOKEN = 4;
|
|
86
89
|
function estimateTokens(text) {
|
|
87
90
|
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
@@ -134,6 +137,32 @@ function scoreSeverity(item) {
|
|
|
134
137
|
if (!item.severity) return 0.5;
|
|
135
138
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
136
139
|
}
|
|
140
|
+
function stem(word) {
|
|
141
|
+
return word.length > 3 ? word.replace(/s$/, "") : word;
|
|
142
|
+
}
|
|
143
|
+
function stems(text) {
|
|
144
|
+
return new Set(
|
|
145
|
+
text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function titleShare(title, keywords) {
|
|
149
|
+
const wanted = stems(keywords.join(" "));
|
|
150
|
+
if (wanted.size === 0) return 0;
|
|
151
|
+
const held = stems(title);
|
|
152
|
+
let matches = 0;
|
|
153
|
+
for (const word of wanted) {
|
|
154
|
+
if (held.has(word)) matches++;
|
|
155
|
+
}
|
|
156
|
+
return matches / wanted.size;
|
|
157
|
+
}
|
|
158
|
+
function scoreReference(item, keywords) {
|
|
159
|
+
const share = titleShare(item.title, keywords);
|
|
160
|
+
const score = Math.max(
|
|
161
|
+
item.relevance ?? 0,
|
|
162
|
+
share >= MIN_TITLE_SHARE ? share : 0
|
|
163
|
+
);
|
|
164
|
+
return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
|
|
165
|
+
}
|
|
137
166
|
function extractKeywords(text) {
|
|
138
167
|
const stopWords = /* @__PURE__ */ new Set([
|
|
139
168
|
"the",
|
|
@@ -230,7 +259,8 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
230
259
|
const keywordScore = scoreKeywordMatch(item, keywords);
|
|
231
260
|
const recencyScore = scoreRecency(item);
|
|
232
261
|
const severityScore = scoreSeverity(item);
|
|
233
|
-
const
|
|
262
|
+
const isReference = item.type === "spec" || item.type === "doc";
|
|
263
|
+
const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
234
264
|
return {
|
|
235
265
|
type: item.type,
|
|
236
266
|
id: item.id,
|
|
@@ -239,7 +269,7 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
239
269
|
score,
|
|
240
270
|
tokenEstimate: estimateTokens(item.content)
|
|
241
271
|
};
|
|
242
|
-
}).sort((a, b) => b.score - a.score);
|
|
272
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
|
243
273
|
}
|
|
244
274
|
function selectWithinBudget(items, maxTokens) {
|
|
245
275
|
const selected = [];
|
|
@@ -252,6 +282,15 @@ function selectWithinBudget(items, maxTokens) {
|
|
|
252
282
|
return { selected, tokensUsed, totalItems: items.length };
|
|
253
283
|
}
|
|
254
284
|
|
|
285
|
+
// src/lib/spec-label.ts
|
|
286
|
+
var DECISION_PREFIX = "ADR: ";
|
|
287
|
+
function isDecisionDraft(name) {
|
|
288
|
+
return name.startsWith(DECISION_PREFIX);
|
|
289
|
+
}
|
|
290
|
+
function specLabel(spec) {
|
|
291
|
+
return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
|
|
292
|
+
}
|
|
293
|
+
|
|
255
294
|
// src/lib/context-builder.ts
|
|
256
295
|
var DEFAULT_BUDGET = 4e3;
|
|
257
296
|
var RESERVE_TOKENS = 200;
|
|
@@ -261,6 +300,10 @@ var MAX_HYDRATED = 8;
|
|
|
261
300
|
var MAX_TAIL = 10;
|
|
262
301
|
var TAIL_SHARE = 0.15;
|
|
263
302
|
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
303
|
+
var STACK_SHARE = 0.1;
|
|
304
|
+
var MIN_REFERENCES = 3;
|
|
305
|
+
var SEARCH_RESULTS = 30;
|
|
306
|
+
var MAX_QUERY_CHARS = 300;
|
|
264
307
|
function formatGuardrail(g) {
|
|
265
308
|
let text = `## ${g.title} [${g.severity}]
|
|
266
309
|
|
|
@@ -279,7 +322,7 @@ ${g.fix}
|
|
|
279
322
|
}
|
|
280
323
|
function formatSpecHeadline(s) {
|
|
281
324
|
return `### ${s.name}
|
|
282
|
-
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s
|
|
325
|
+
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
|
|
283
326
|
}
|
|
284
327
|
function formatDocHeadline(d) {
|
|
285
328
|
return `### ${d.title}
|
|
@@ -290,6 +333,21 @@ function formatStack(entries) {
|
|
|
290
333
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
291
334
|
).join("\n");
|
|
292
335
|
}
|
|
336
|
+
function selectStack(entries, words, maxTokens) {
|
|
337
|
+
const named = (e) => {
|
|
338
|
+
const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
|
|
339
|
+
return words.some((w) => entry.includes(w));
|
|
340
|
+
};
|
|
341
|
+
const ordered = [...entries.filter(named), ...entries.filter((e) => !named(e))];
|
|
342
|
+
const shown = [];
|
|
343
|
+
for (const entry of ordered) {
|
|
344
|
+
if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
shown.push(entry);
|
|
348
|
+
}
|
|
349
|
+
return { text: formatStack(shown), hidden: entries.length - shown.length };
|
|
350
|
+
}
|
|
293
351
|
function formatRules(rules) {
|
|
294
352
|
const byCategory = /* @__PURE__ */ new Map();
|
|
295
353
|
for (const r of rules) {
|
|
@@ -311,6 +369,21 @@ function formatRules(rules) {
|
|
|
311
369
|
|
|
312
370
|
${text.trim()}`;
|
|
313
371
|
}
|
|
372
|
+
async function findDocs(api, taskDescription) {
|
|
373
|
+
const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
|
|
374
|
+
if (query) {
|
|
375
|
+
try {
|
|
376
|
+
const found = await api.get("/v1/docs", {
|
|
377
|
+
q: query,
|
|
378
|
+
limit: String(SEARCH_RESULTS)
|
|
379
|
+
});
|
|
380
|
+
if (found.items.length > 0) return { ...found, searched: true };
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const listed = await api.get("/v1/docs");
|
|
385
|
+
return { ...listed, searched: false };
|
|
386
|
+
}
|
|
314
387
|
async function fetchSpecBody(api, draftId) {
|
|
315
388
|
const spec = await api.get(
|
|
316
389
|
`/v1/specs/${encodeURIComponent(draftId)}`
|
|
@@ -411,7 +484,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
411
484
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
412
485
|
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
413
486
|
api.get("/v1/specs").catch(orEmpty([])),
|
|
414
|
-
api
|
|
487
|
+
findDocs(api, taskDescription).catch(
|
|
488
|
+
orEmpty({ items: [], searched: false })
|
|
489
|
+
),
|
|
415
490
|
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
416
491
|
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
417
492
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
@@ -424,6 +499,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
424
499
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
425
500
|
const refs = /* @__PURE__ */ new Map();
|
|
426
501
|
const key = (type, id) => `${type}:${id}`;
|
|
502
|
+
const docTitles = new Set(docs.items.map((d) => d.title));
|
|
503
|
+
const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
|
|
427
504
|
const scorable = [
|
|
428
505
|
...guardrails.map((g) => ({
|
|
429
506
|
type: "guardrail",
|
|
@@ -434,12 +511,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
434
511
|
severity: g.severity,
|
|
435
512
|
updatedAt: g.updatedAt
|
|
436
513
|
})),
|
|
437
|
-
...specs.filter((s) => s.status !== "archived").map((s) => {
|
|
514
|
+
...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
|
|
438
515
|
refs.set(key("spec", s.draftId), {
|
|
439
516
|
type: "spec",
|
|
440
517
|
id: s.draftId,
|
|
441
518
|
title: s.name,
|
|
442
|
-
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s
|
|
519
|
+
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
|
|
443
520
|
fetchHint: `get_specs(draft_id="${s.draftId}")`
|
|
444
521
|
});
|
|
445
522
|
return {
|
|
@@ -450,7 +527,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
450
527
|
updatedAt: s.updatedAt
|
|
451
528
|
};
|
|
452
529
|
}),
|
|
453
|
-
...docs.items.map((d) => {
|
|
530
|
+
...docs.items.map((d, position) => {
|
|
454
531
|
const section = selectSection(d.outline, taskDescription);
|
|
455
532
|
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
456
533
|
refs.set(key("doc", d.id), {
|
|
@@ -469,7 +546,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
469
546
|
title: d.title,
|
|
470
547
|
content: formatDocHeadline(d),
|
|
471
548
|
category: d.doc_type === "adr" ? "architecture" : void 0,
|
|
472
|
-
updatedAt: d.synced_at
|
|
549
|
+
updatedAt: d.synced_at,
|
|
550
|
+
path: d.path,
|
|
551
|
+
relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
|
|
473
552
|
};
|
|
474
553
|
})
|
|
475
554
|
];
|
|
@@ -477,9 +556,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
477
556
|
const sections = [];
|
|
478
557
|
let overhead = 0;
|
|
479
558
|
if (stack.length > 0) {
|
|
559
|
+
const { text, hidden } = selectStack(
|
|
560
|
+
stack,
|
|
561
|
+
[...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
|
|
562
|
+
Math.floor(budget * STACK_SHARE)
|
|
563
|
+
);
|
|
480
564
|
const section = `## Tech Stack
|
|
481
565
|
|
|
482
|
-
${
|
|
566
|
+
${text}` + (hidden > 0 ? `
|
|
567
|
+
|
|
568
|
+
_${hidden} more with \`get_tech_stack\`._` : "");
|
|
483
569
|
sections.push(section);
|
|
484
570
|
overhead += estimateTokens(section);
|
|
485
571
|
}
|
|
@@ -598,9 +684,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
|
|
|
598
684
|
const tailLines = [];
|
|
599
685
|
for (const meta of tailCandidates) {
|
|
600
686
|
const cost = estimateTokens(meta.listLine);
|
|
601
|
-
|
|
687
|
+
const named = blocks.length + tailLines.length;
|
|
688
|
+
if (cost > remaining && named >= MIN_REFERENCES) break;
|
|
602
689
|
tailLines.push(meta.listLine);
|
|
603
|
-
remaining
|
|
690
|
+
remaining = Math.max(0, remaining - cost);
|
|
604
691
|
}
|
|
605
692
|
if (tailLines.length > 0) {
|
|
606
693
|
sections.push(
|
package/dist/lib/scoring.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ interface ScorableItem {
|
|
|
15
15
|
category?: string;
|
|
16
16
|
severity?: string;
|
|
17
17
|
updatedAt?: string;
|
|
18
|
+
/** Where a document's repository file lives. */
|
|
19
|
+
path?: string;
|
|
20
|
+
/** 0 to 1, from the order the document search returned it in. */
|
|
21
|
+
relevance?: number;
|
|
18
22
|
}
|
|
19
23
|
export declare function estimateTokens(text: string): number;
|
|
20
24
|
/**
|
|
@@ -26,6 +30,7 @@ export declare function truncateToTokens(text: string, maxTokens: number): {
|
|
|
26
30
|
text: string;
|
|
27
31
|
truncated: boolean;
|
|
28
32
|
};
|
|
33
|
+
export declare function extractKeywords(text: string): string[];
|
|
29
34
|
export declare function scoreItems(items: ScorableItem[], taskDescription: string, filePaths: string[]): ScoredItem[];
|
|
30
35
|
export declare function selectWithinBudget(items: ScoredItem[], maxTokens: number): {
|
|
31
36
|
selected: ScoredItem[];
|
|
@@ -93,7 +93,7 @@ export interface TeamSetup {
|
|
|
93
93
|
/** Absent on an API older than ADR-223. */
|
|
94
94
|
sessionBrief?: SessionBrief;
|
|
95
95
|
}
|
|
96
|
-
/** What every session is told (ADR-223, specs/
|
|
96
|
+
/** What every session is told (ADR-223, specs/archive/session-brief/contract.md). */
|
|
97
97
|
export interface SessionBrief {
|
|
98
98
|
text: string;
|
|
99
99
|
/** What a session may install by itself: in the default space, and unable to run anything by itself. */
|