truecourse 0.7.0-next.7 → 0.7.0-next.8
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/cli.mjs +308 -13
- package/package.json +1 -1
- package/roslyn-host/csharp-roslyn-host.dll +0 -0
- package/roslyn-host/csharp-roslyn-host.pdb +0 -0
- package/server.mjs +310 -15
package/cli.mjs
CHANGED
|
@@ -171176,7 +171176,7 @@ function readToolVersion() {
|
|
|
171176
171176
|
if (cachedVersion)
|
|
171177
171177
|
return cachedVersion;
|
|
171178
171178
|
if (true) {
|
|
171179
|
-
cachedVersion = "0.7.0-next.
|
|
171179
|
+
cachedVersion = "0.7.0-next.8";
|
|
171180
171180
|
return cachedVersion;
|
|
171181
171181
|
}
|
|
171182
171182
|
try {
|
|
@@ -197210,7 +197210,17 @@ var OverlapSectionSchema = external_exports.object({
|
|
|
197210
197210
|
* from older corpora still parses; `null` is the preamble marker the viewer
|
|
197211
197211
|
* bands as the pre-first-heading block.
|
|
197212
197212
|
*/
|
|
197213
|
-
heading: external_exports.string().nullable()
|
|
197213
|
+
heading: external_exports.string().nullable(),
|
|
197214
|
+
/**
|
|
197215
|
+
* A short verbatim excerpt (≤ ~25 words) of the disputed sentence, copied from
|
|
197216
|
+
* the doc — the model's evidence for the heading it picked. Persisted (optional,
|
|
197217
|
+
* so older corpora without it still parse) for verification transparency and so
|
|
197218
|
+
* the viewer can later highlight the exact disputed sentence, not just band the
|
|
197219
|
+
* section. Consumed at assembly by `verifyOverlapSections` to anchor the pointer
|
|
197220
|
+
* by exact location; NOT part of the cross-area dedup identity (that stays
|
|
197221
|
+
* doc + heading).
|
|
197222
|
+
*/
|
|
197223
|
+
quote: external_exports.string().optional()
|
|
197214
197224
|
});
|
|
197215
197225
|
var OverlapSchema = external_exports.object({
|
|
197216
197226
|
/** The two docs that overlap, by ref. */
|
|
@@ -198003,6 +198013,237 @@ import { createHash as createHash7 } from "node:crypto";
|
|
|
198003
198013
|
import fs26 from "node:fs";
|
|
198004
198014
|
init_transport();
|
|
198005
198015
|
init_dist5();
|
|
198016
|
+
|
|
198017
|
+
// packages/spec-consolidator/dist/pointer-verifier.js
|
|
198018
|
+
var MIN_MEANINGFUL_SCORE = 1.5;
|
|
198019
|
+
var NEGLIGIBLE_RATIO = 0.25;
|
|
198020
|
+
var STOPWORDS2 = /* @__PURE__ */ new Set([
|
|
198021
|
+
"the",
|
|
198022
|
+
"a",
|
|
198023
|
+
"an",
|
|
198024
|
+
"and",
|
|
198025
|
+
"or",
|
|
198026
|
+
"but",
|
|
198027
|
+
"if",
|
|
198028
|
+
"then",
|
|
198029
|
+
"else",
|
|
198030
|
+
"of",
|
|
198031
|
+
"to",
|
|
198032
|
+
"in",
|
|
198033
|
+
"on",
|
|
198034
|
+
"at",
|
|
198035
|
+
"by",
|
|
198036
|
+
"for",
|
|
198037
|
+
"with",
|
|
198038
|
+
"as",
|
|
198039
|
+
"is",
|
|
198040
|
+
"are",
|
|
198041
|
+
"was",
|
|
198042
|
+
"were",
|
|
198043
|
+
"be",
|
|
198044
|
+
"been",
|
|
198045
|
+
"being",
|
|
198046
|
+
"it",
|
|
198047
|
+
"its",
|
|
198048
|
+
"this",
|
|
198049
|
+
"that",
|
|
198050
|
+
"these",
|
|
198051
|
+
"those",
|
|
198052
|
+
"they",
|
|
198053
|
+
"them",
|
|
198054
|
+
"their",
|
|
198055
|
+
"there",
|
|
198056
|
+
"here",
|
|
198057
|
+
"no",
|
|
198058
|
+
"not",
|
|
198059
|
+
"yes",
|
|
198060
|
+
"so",
|
|
198061
|
+
"than",
|
|
198062
|
+
"too",
|
|
198063
|
+
"very",
|
|
198064
|
+
"can",
|
|
198065
|
+
"could",
|
|
198066
|
+
"will",
|
|
198067
|
+
"would",
|
|
198068
|
+
"shall",
|
|
198069
|
+
"should",
|
|
198070
|
+
"may",
|
|
198071
|
+
"might",
|
|
198072
|
+
"must",
|
|
198073
|
+
"do",
|
|
198074
|
+
"does",
|
|
198075
|
+
"did",
|
|
198076
|
+
"done",
|
|
198077
|
+
"has",
|
|
198078
|
+
"have",
|
|
198079
|
+
"had",
|
|
198080
|
+
"from",
|
|
198081
|
+
"up",
|
|
198082
|
+
"out",
|
|
198083
|
+
"down",
|
|
198084
|
+
"over",
|
|
198085
|
+
"under",
|
|
198086
|
+
"again",
|
|
198087
|
+
"we",
|
|
198088
|
+
"you",
|
|
198089
|
+
"your",
|
|
198090
|
+
"our",
|
|
198091
|
+
"us",
|
|
198092
|
+
"i",
|
|
198093
|
+
"he",
|
|
198094
|
+
"she",
|
|
198095
|
+
"his",
|
|
198096
|
+
"her",
|
|
198097
|
+
"each",
|
|
198098
|
+
"any",
|
|
198099
|
+
"all",
|
|
198100
|
+
"both",
|
|
198101
|
+
"some",
|
|
198102
|
+
"such",
|
|
198103
|
+
"only",
|
|
198104
|
+
"own",
|
|
198105
|
+
"same",
|
|
198106
|
+
"more",
|
|
198107
|
+
"most",
|
|
198108
|
+
"other",
|
|
198109
|
+
"into",
|
|
198110
|
+
"about",
|
|
198111
|
+
"when",
|
|
198112
|
+
"where",
|
|
198113
|
+
"which",
|
|
198114
|
+
"who",
|
|
198115
|
+
"whom",
|
|
198116
|
+
"what",
|
|
198117
|
+
"how",
|
|
198118
|
+
"why",
|
|
198119
|
+
"per",
|
|
198120
|
+
"via",
|
|
198121
|
+
"also"
|
|
198122
|
+
]);
|
|
198123
|
+
function tokenize(text4, drop) {
|
|
198124
|
+
const lowered = text4.toLowerCase().replace(/[`*_~#>]/g, " ");
|
|
198125
|
+
const out = [];
|
|
198126
|
+
for (const raw of lowered.split(/[^a-z0-9]+/)) {
|
|
198127
|
+
if (raw.length < 2)
|
|
198128
|
+
continue;
|
|
198129
|
+
if (STOPWORDS2.has(raw))
|
|
198130
|
+
continue;
|
|
198131
|
+
if (drop.has(raw))
|
|
198132
|
+
continue;
|
|
198133
|
+
out.push(raw);
|
|
198134
|
+
}
|
|
198135
|
+
return out;
|
|
198136
|
+
}
|
|
198137
|
+
function pathWords(p2) {
|
|
198138
|
+
return p2.toLowerCase().split(/[^a-z0-9]+/).filter((t2) => t2.length >= 2);
|
|
198139
|
+
}
|
|
198140
|
+
var HEADING_RE = /^ {0,3}#{1,6}\s+(.*)$/;
|
|
198141
|
+
function splitDocSections(body, drop) {
|
|
198142
|
+
const raws = [];
|
|
198143
|
+
let cur = { heading: "", text: "" };
|
|
198144
|
+
for (const line of body.split(/\r?\n/)) {
|
|
198145
|
+
const m = HEADING_RE.exec(line);
|
|
198146
|
+
if (m) {
|
|
198147
|
+
if (cur.text.trim() || cur.heading)
|
|
198148
|
+
raws.push(cur);
|
|
198149
|
+
cur = { heading: m[1].trim(), text: `${line}
|
|
198150
|
+
` };
|
|
198151
|
+
} else {
|
|
198152
|
+
cur.text += `${line}
|
|
198153
|
+
`;
|
|
198154
|
+
}
|
|
198155
|
+
}
|
|
198156
|
+
if (cur.text.trim() || cur.heading)
|
|
198157
|
+
raws.push(cur);
|
|
198158
|
+
return raws.map((r) => ({
|
|
198159
|
+
realHeading: r.heading === "" ? null : r.heading,
|
|
198160
|
+
tokens: new Set(tokenize(r.text, drop)),
|
|
198161
|
+
text: r.text
|
|
198162
|
+
}));
|
|
198163
|
+
}
|
|
198164
|
+
function headingKey(h) {
|
|
198165
|
+
return h.replace(/[`*_~]/g, "").trim().toLowerCase();
|
|
198166
|
+
}
|
|
198167
|
+
function normalizeForQuote(text4) {
|
|
198168
|
+
return text4.replace(/[`*_~]/g, "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
198169
|
+
}
|
|
198170
|
+
function locateQuote(sections, quote) {
|
|
198171
|
+
const needle = normalizeForQuote(quote);
|
|
198172
|
+
if (!needle)
|
|
198173
|
+
return [];
|
|
198174
|
+
const hits = [];
|
|
198175
|
+
for (let i = 0; i < sections.length; i++) {
|
|
198176
|
+
if (normalizeForQuote(sections[i].text).includes(needle))
|
|
198177
|
+
hits.push(i);
|
|
198178
|
+
}
|
|
198179
|
+
return hits;
|
|
198180
|
+
}
|
|
198181
|
+
function scoreSections(sections, noteTokens) {
|
|
198182
|
+
const N = sections.length;
|
|
198183
|
+
const df = /* @__PURE__ */ new Map();
|
|
198184
|
+
for (const s of sections)
|
|
198185
|
+
for (const t2 of s.tokens)
|
|
198186
|
+
df.set(t2, (df.get(t2) ?? 0) + 1);
|
|
198187
|
+
const idf = (t2) => {
|
|
198188
|
+
const d3 = df.get(t2) ?? 0;
|
|
198189
|
+
if (d3 === 0)
|
|
198190
|
+
return 0;
|
|
198191
|
+
return Math.max(0, Math.log2((N + 1) / (d3 + 1)));
|
|
198192
|
+
};
|
|
198193
|
+
return sections.map((s) => {
|
|
198194
|
+
let score = 0;
|
|
198195
|
+
for (const t2 of noteTokens)
|
|
198196
|
+
if (s.tokens.has(t2))
|
|
198197
|
+
score += idf(t2);
|
|
198198
|
+
return score;
|
|
198199
|
+
});
|
|
198200
|
+
}
|
|
198201
|
+
function verifyOverlapSections(input) {
|
|
198202
|
+
const { docs, note, sections, bodyOf } = input;
|
|
198203
|
+
if (sections.length === 0)
|
|
198204
|
+
return [...sections];
|
|
198205
|
+
const drop = /* @__PURE__ */ new Set([...pathWords(docs[0]), ...pathWords(docs[1])]);
|
|
198206
|
+
const noteTokens = new Set(tokenize(note, drop));
|
|
198207
|
+
return sections.map((ptr) => {
|
|
198208
|
+
const body = bodyOf(ptr.doc);
|
|
198209
|
+
if (body === void 0)
|
|
198210
|
+
return { ...ptr };
|
|
198211
|
+
const candidates = splitDocSections(body, drop);
|
|
198212
|
+
if (candidates.length === 0)
|
|
198213
|
+
return { ...ptr };
|
|
198214
|
+
let pointedIdx;
|
|
198215
|
+
if (ptr.heading === null) {
|
|
198216
|
+
pointedIdx = 0;
|
|
198217
|
+
} else {
|
|
198218
|
+
const key4 = headingKey(ptr.heading);
|
|
198219
|
+
pointedIdx = candidates.findIndex((c2) => c2.realHeading !== null && headingKey(c2.realHeading) === key4);
|
|
198220
|
+
}
|
|
198221
|
+
if (ptr.quote !== void 0) {
|
|
198222
|
+
const hits = locateQuote(candidates, ptr.quote);
|
|
198223
|
+
if (hits.length > 0) {
|
|
198224
|
+
if (pointedIdx >= 0 && hits.includes(pointedIdx))
|
|
198225
|
+
return { ...ptr };
|
|
198226
|
+
const target = hits[0];
|
|
198227
|
+
return { ...ptr, heading: target === 0 ? null : candidates[target].realHeading };
|
|
198228
|
+
}
|
|
198229
|
+
}
|
|
198230
|
+
if (noteTokens.size === 0)
|
|
198231
|
+
return { ...ptr };
|
|
198232
|
+
const scores = scoreSections(candidates, noteTokens);
|
|
198233
|
+
let bestIdx = 0;
|
|
198234
|
+
for (let i = 1; i < scores.length; i++)
|
|
198235
|
+
if (scores[i] > scores[bestIdx])
|
|
198236
|
+
bestIdx = i;
|
|
198237
|
+
const bestScore = scores[bestIdx];
|
|
198238
|
+
const pointedScore = pointedIdx >= 0 ? scores[pointedIdx] : 0;
|
|
198239
|
+
const reanchor = bestIdx !== pointedIdx && bestScore >= MIN_MEANINGFUL_SCORE && pointedScore < MIN_MEANINGFUL_SCORE && pointedScore <= NEGLIGIBLE_RATIO * bestScore;
|
|
198240
|
+
if (!reanchor)
|
|
198241
|
+
return { ...ptr };
|
|
198242
|
+
return { ...ptr, heading: bestIdx === 0 ? null : candidates[bestIdx].realHeading };
|
|
198243
|
+
});
|
|
198244
|
+
}
|
|
198245
|
+
|
|
198246
|
+
// packages/spec-consolidator/dist/overlap-detector.js
|
|
198006
198247
|
var DEFAULT_MAX_PAIRS_PER_AREA = 60;
|
|
198007
198248
|
async function flagOverlaps(repoRoot5, areas, docs, opts = {}) {
|
|
198008
198249
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -198092,6 +198333,19 @@ async function flagOverlaps(repoRoot5, areas, docs, opts = {}) {
|
|
|
198092
198333
|
};
|
|
198093
198334
|
launch();
|
|
198094
198335
|
});
|
|
198336
|
+
for (const list of result.values()) {
|
|
198337
|
+
for (const overlap of list) {
|
|
198338
|
+
overlap.sections = verifyOverlapSections({
|
|
198339
|
+
docs: overlap.docs,
|
|
198340
|
+
note: overlap.note,
|
|
198341
|
+
sections: overlap.sections,
|
|
198342
|
+
bodyOf: (ref) => {
|
|
198343
|
+
const d3 = byPath.get(ref);
|
|
198344
|
+
return d3 ? docBody2(d3) : void 0;
|
|
198345
|
+
}
|
|
198346
|
+
});
|
|
198347
|
+
}
|
|
198348
|
+
}
|
|
198095
198349
|
const entries = [...result].flatMap(([area, list]) => list.map((overlap) => ({ area, overlap })));
|
|
198096
198350
|
const merged = dedupeCrossAreaOverlaps(entries);
|
|
198097
198351
|
result.clear();
|
|
@@ -198146,9 +198400,12 @@ NOT a disagreement (do NOT flag):
|
|
|
198146
198400
|
|
|
198147
198401
|
Bias: when there is a PLAUSIBLE contradiction a human should check, flag it. When the docs are clearly complementary or agree, do not.
|
|
198148
198402
|
|
|
198149
|
-
When you flag an overlap,
|
|
198403
|
+
When you flag an overlap, point at WHERE each side's disputed claim lives. Below each doc you are given a CLOSED list of that doc's section options \u2014 its headings, plus a "lead" option. Do NOT recall or guess a heading; SELECT from the list. For EACH side output a pointer with two fields:
|
|
198404
|
+
- \`heading\`: EXACTLY one of that doc's listed section headings, copied verbatim \u2014 OR the JSON literal \`null\` (not the string "null", not "") for the LEAD, the text ABOVE its first heading (or, when the doc opens straight with a title, that opening title block). Never emit a heading that is not one of the listed options.
|
|
198405
|
+
- \`quote\`: a SHORT verbatim excerpt (\u2264 25 words) of the disputed sentence, copied EXACTLY from that doc \u2014 the words that state the claim in dispute. This is your evidence for the heading you picked; copy it, do not paraphrase.
|
|
198406
|
+
List one entry per side; omit a side only when it genuinely has no conflicting passage.
|
|
198150
198407
|
|
|
198151
|
-
PREAMBLE:
|
|
198408
|
+
PREAMBLE: use \`heading\`: \`null\` ONLY for that lead/preamble block; whenever the disputed passage is under a listed heading, select that heading verbatim.
|
|
198152
198409
|
|
|
198153
198410
|
In the NOTE, refer to each doc by its FILENAME (the basename shown in the header, e.g. \`users.md\`) \u2014 NEVER "doc A" / "doc B", which mean nothing to the reader.
|
|
198154
198411
|
|
|
@@ -198156,13 +198413,19 @@ Output ONLY a JSON object, no prose, no code fences:
|
|
|
198156
198413
|
|
|
198157
198414
|
{ "overlap": true,
|
|
198158
198415
|
"note": "users.md uses auth0_id; identity.md uses auth0_sub for the same user column",
|
|
198159
|
-
"sections": [
|
|
198416
|
+
"sections": [
|
|
198417
|
+
{ "side": "A", "heading": "User model", "quote": "the auth0_id column stores the Auth0 subject" },
|
|
198418
|
+
{ "side": "B", "heading": "Identity", "quote": "we persist the Auth0 subject in auth0_sub" }
|
|
198419
|
+
] }
|
|
198160
198420
|
|
|
198161
|
-
Preamble example \u2014 doc A's claim is a README
|
|
198421
|
+
Preamble example \u2014 doc A's disputed claim is a README tagline ABOVE its first heading, so its side uses \`heading\`: null and quotes that tagline verbatim:
|
|
198162
198422
|
|
|
198163
198423
|
{ "overlap": true,
|
|
198164
198424
|
"note": "README.md lists C# as a supported language in the preamble; plan.md's Tech Stack omits it",
|
|
198165
|
-
"sections": [
|
|
198425
|
+
"sections": [
|
|
198426
|
+
{ "side": "A", "heading": null, "quote": "Supports TypeScript, Python, and C# out of the box" },
|
|
198427
|
+
{ "side": "B", "heading": "Tech Stack", "quote": "The stack is TypeScript and Python" }
|
|
198428
|
+
] }
|
|
198166
198429
|
|
|
198167
198430
|
Use { "overlap": false, "note": "", "sections": [] } when they are complementary or agree. The note is shown to the user \u2014 name the specific thing that differs.`;
|
|
198168
198431
|
var OVERLAP_PREVIEW_LINES = 120;
|
|
@@ -198196,31 +198459,59 @@ function hasConcernHeading(doc, concern, vocab) {
|
|
|
198196
198459
|
}
|
|
198197
198460
|
return false;
|
|
198198
198461
|
}
|
|
198462
|
+
function sectionHeadings(body) {
|
|
198463
|
+
const out = [];
|
|
198464
|
+
for (const line of body.split(/\r?\n/)) {
|
|
198465
|
+
const m = /^ {0,3}#{1,6}\s+(.*)$/.exec(line);
|
|
198466
|
+
if (!m)
|
|
198467
|
+
continue;
|
|
198468
|
+
const text4 = m[1].replace(/\s*#*\s*$/, "").trim();
|
|
198469
|
+
if (text4)
|
|
198470
|
+
out.push(text4);
|
|
198471
|
+
}
|
|
198472
|
+
return out;
|
|
198473
|
+
}
|
|
198474
|
+
function sectionOptions(slice5) {
|
|
198475
|
+
const lines = [" - the lead (text above the first heading, or the opening title block) \u2192 use heading: null"];
|
|
198476
|
+
for (const h of sectionHeadings(slice5))
|
|
198477
|
+
lines.push(` - ${h}`);
|
|
198478
|
+
return lines;
|
|
198479
|
+
}
|
|
198199
198480
|
function buildOverlapUserPrompt(areaId, a, b) {
|
|
198200
198481
|
const slice5 = (d3) => docBody2(d3).split(/\r?\n/).slice(0, OVERLAP_PREVIEW_LINES).join("\n");
|
|
198482
|
+
const sliceA = slice5(a);
|
|
198483
|
+
const sliceB = slice5(b);
|
|
198201
198484
|
return [
|
|
198202
198485
|
`Area: ${areaId}`,
|
|
198203
198486
|
"",
|
|
198204
198487
|
`--- doc A: ${a.path} ---`,
|
|
198205
|
-
|
|
198488
|
+
sliceA,
|
|
198206
198489
|
`--- end doc A ---`,
|
|
198207
198490
|
"",
|
|
198208
198491
|
`--- doc B: ${b.path} ---`,
|
|
198209
|
-
|
|
198492
|
+
sliceB,
|
|
198210
198493
|
`--- end doc B ---`,
|
|
198211
198494
|
"",
|
|
198495
|
+
"SECTION OPTIONS \u2014 each side pointer MUST be one of these (verbatim), or the lead (heading: null):",
|
|
198496
|
+
"",
|
|
198497
|
+
`doc A (${a.path}):`,
|
|
198498
|
+
...sectionOptions(sliceA),
|
|
198499
|
+
"",
|
|
198500
|
+
`doc B (${b.path}):`,
|
|
198501
|
+
...sectionOptions(sliceB),
|
|
198502
|
+
"",
|
|
198212
198503
|
"Return the JSON object as specified."
|
|
198213
198504
|
].join("\n");
|
|
198214
198505
|
}
|
|
198215
198506
|
var LlmOverlapSchema = external_exports.object({
|
|
198216
198507
|
overlap: external_exports.boolean(),
|
|
198217
198508
|
note: external_exports.string().default(""),
|
|
198218
|
-
sections: external_exports.array(external_exports.object({ side: external_exports.enum(["A", "B"]), heading: external_exports.string().nullable() })).default([])
|
|
198509
|
+
sections: external_exports.array(external_exports.object({ side: external_exports.enum(["A", "B"]), heading: external_exports.string().nullable(), quote: external_exports.string().optional() })).default([])
|
|
198219
198510
|
});
|
|
198220
198511
|
var OverlapVerdictSchema = external_exports.object({
|
|
198221
198512
|
overlap: external_exports.boolean(),
|
|
198222
198513
|
note: external_exports.string().default(""),
|
|
198223
|
-
sections: external_exports.array(external_exports.object({ doc: external_exports.string(), heading: external_exports.string().nullable() })).default([])
|
|
198514
|
+
sections: external_exports.array(external_exports.object({ doc: external_exports.string(), heading: external_exports.string().nullable(), quote: external_exports.string().optional() })).default([])
|
|
198224
198515
|
});
|
|
198225
198516
|
function spawnOverlapRunner(opts = {}) {
|
|
198226
198517
|
const transport = opts.transport ?? cliTransport({ bin: opts.bin });
|
|
@@ -198240,7 +198531,11 @@ function spawnOverlapRunner(opts = {}) {
|
|
|
198240
198531
|
return {
|
|
198241
198532
|
overlap: inner.overlap,
|
|
198242
198533
|
note: inner.note,
|
|
198243
|
-
sections: inner.sections.map((s) => ({
|
|
198534
|
+
sections: inner.sections.map((s) => ({
|
|
198535
|
+
doc: s.side === "A" ? a.path : b.path,
|
|
198536
|
+
heading: s.heading,
|
|
198537
|
+
...s.quote !== void 0 ? { quote: s.quote } : {}
|
|
198538
|
+
}))
|
|
198244
198539
|
};
|
|
198245
198540
|
};
|
|
198246
198541
|
}
|
|
@@ -212438,7 +212733,7 @@ async function runHooksRun() {
|
|
|
212438
212733
|
|
|
212439
212734
|
// tools/cli/src/index.ts
|
|
212440
212735
|
var program2 = new Command();
|
|
212441
|
-
program2.name("truecourse").version("0.7.0-next.
|
|
212736
|
+
program2.name("truecourse").version("0.7.0-next.8").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
|
|
212442
212737
|
function warnDiscontinued(command) {
|
|
212443
212738
|
process.stderr.write(
|
|
212444
212739
|
`\u26A0 \`${command}\` is discontinued in favor of \`truecourse guard\` \u2014 see the README. Planned removal: 0.8.
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/server.mjs
CHANGED
|
@@ -82898,11 +82898,11 @@ var init_entropy = __esm({
|
|
|
82898
82898
|
});
|
|
82899
82899
|
|
|
82900
82900
|
// packages/analyzer/dist/rules/security/stopwords.js
|
|
82901
|
-
var
|
|
82901
|
+
var STOPWORDS2;
|
|
82902
82902
|
var init_stopwords = __esm({
|
|
82903
82903
|
"packages/analyzer/dist/rules/security/stopwords.js"() {
|
|
82904
82904
|
"use strict";
|
|
82905
|
-
|
|
82905
|
+
STOPWORDS2 = [
|
|
82906
82906
|
// Programming concepts
|
|
82907
82907
|
"abstract",
|
|
82908
82908
|
"accessor",
|
|
@@ -85637,7 +85637,7 @@ function scanForSecrets(value, context) {
|
|
|
85637
85637
|
}
|
|
85638
85638
|
if (rule.useStopwords) {
|
|
85639
85639
|
const secretLower = secretValue.toLowerCase();
|
|
85640
|
-
if (
|
|
85640
|
+
if (STOPWORDS2.some((sw) => secretLower.includes(sw)))
|
|
85641
85641
|
continue;
|
|
85642
85642
|
}
|
|
85643
85643
|
if (rule.allowlist) {
|
|
@@ -197154,7 +197154,17 @@ var OverlapSectionSchema = external_exports.object({
|
|
|
197154
197154
|
* from older corpora still parses; `null` is the preamble marker the viewer
|
|
197155
197155
|
* bands as the pre-first-heading block.
|
|
197156
197156
|
*/
|
|
197157
|
-
heading: external_exports.string().nullable()
|
|
197157
|
+
heading: external_exports.string().nullable(),
|
|
197158
|
+
/**
|
|
197159
|
+
* A short verbatim excerpt (≤ ~25 words) of the disputed sentence, copied from
|
|
197160
|
+
* the doc — the model's evidence for the heading it picked. Persisted (optional,
|
|
197161
|
+
* so older corpora without it still parse) for verification transparency and so
|
|
197162
|
+
* the viewer can later highlight the exact disputed sentence, not just band the
|
|
197163
|
+
* section. Consumed at assembly by `verifyOverlapSections` to anchor the pointer
|
|
197164
|
+
* by exact location; NOT part of the cross-area dedup identity (that stays
|
|
197165
|
+
* doc + heading).
|
|
197166
|
+
*/
|
|
197167
|
+
quote: external_exports.string().optional()
|
|
197158
197168
|
});
|
|
197159
197169
|
var OverlapSchema = external_exports.object({
|
|
197160
197170
|
/** The two docs that overlap, by ref. */
|
|
@@ -197947,6 +197957,237 @@ import { createHash as createHash6 } from "node:crypto";
|
|
|
197947
197957
|
import fs28 from "node:fs";
|
|
197948
197958
|
init_transport();
|
|
197949
197959
|
init_dist();
|
|
197960
|
+
|
|
197961
|
+
// packages/spec-consolidator/dist/pointer-verifier.js
|
|
197962
|
+
var MIN_MEANINGFUL_SCORE = 1.5;
|
|
197963
|
+
var NEGLIGIBLE_RATIO = 0.25;
|
|
197964
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
197965
|
+
"the",
|
|
197966
|
+
"a",
|
|
197967
|
+
"an",
|
|
197968
|
+
"and",
|
|
197969
|
+
"or",
|
|
197970
|
+
"but",
|
|
197971
|
+
"if",
|
|
197972
|
+
"then",
|
|
197973
|
+
"else",
|
|
197974
|
+
"of",
|
|
197975
|
+
"to",
|
|
197976
|
+
"in",
|
|
197977
|
+
"on",
|
|
197978
|
+
"at",
|
|
197979
|
+
"by",
|
|
197980
|
+
"for",
|
|
197981
|
+
"with",
|
|
197982
|
+
"as",
|
|
197983
|
+
"is",
|
|
197984
|
+
"are",
|
|
197985
|
+
"was",
|
|
197986
|
+
"were",
|
|
197987
|
+
"be",
|
|
197988
|
+
"been",
|
|
197989
|
+
"being",
|
|
197990
|
+
"it",
|
|
197991
|
+
"its",
|
|
197992
|
+
"this",
|
|
197993
|
+
"that",
|
|
197994
|
+
"these",
|
|
197995
|
+
"those",
|
|
197996
|
+
"they",
|
|
197997
|
+
"them",
|
|
197998
|
+
"their",
|
|
197999
|
+
"there",
|
|
198000
|
+
"here",
|
|
198001
|
+
"no",
|
|
198002
|
+
"not",
|
|
198003
|
+
"yes",
|
|
198004
|
+
"so",
|
|
198005
|
+
"than",
|
|
198006
|
+
"too",
|
|
198007
|
+
"very",
|
|
198008
|
+
"can",
|
|
198009
|
+
"could",
|
|
198010
|
+
"will",
|
|
198011
|
+
"would",
|
|
198012
|
+
"shall",
|
|
198013
|
+
"should",
|
|
198014
|
+
"may",
|
|
198015
|
+
"might",
|
|
198016
|
+
"must",
|
|
198017
|
+
"do",
|
|
198018
|
+
"does",
|
|
198019
|
+
"did",
|
|
198020
|
+
"done",
|
|
198021
|
+
"has",
|
|
198022
|
+
"have",
|
|
198023
|
+
"had",
|
|
198024
|
+
"from",
|
|
198025
|
+
"up",
|
|
198026
|
+
"out",
|
|
198027
|
+
"down",
|
|
198028
|
+
"over",
|
|
198029
|
+
"under",
|
|
198030
|
+
"again",
|
|
198031
|
+
"we",
|
|
198032
|
+
"you",
|
|
198033
|
+
"your",
|
|
198034
|
+
"our",
|
|
198035
|
+
"us",
|
|
198036
|
+
"i",
|
|
198037
|
+
"he",
|
|
198038
|
+
"she",
|
|
198039
|
+
"his",
|
|
198040
|
+
"her",
|
|
198041
|
+
"each",
|
|
198042
|
+
"any",
|
|
198043
|
+
"all",
|
|
198044
|
+
"both",
|
|
198045
|
+
"some",
|
|
198046
|
+
"such",
|
|
198047
|
+
"only",
|
|
198048
|
+
"own",
|
|
198049
|
+
"same",
|
|
198050
|
+
"more",
|
|
198051
|
+
"most",
|
|
198052
|
+
"other",
|
|
198053
|
+
"into",
|
|
198054
|
+
"about",
|
|
198055
|
+
"when",
|
|
198056
|
+
"where",
|
|
198057
|
+
"which",
|
|
198058
|
+
"who",
|
|
198059
|
+
"whom",
|
|
198060
|
+
"what",
|
|
198061
|
+
"how",
|
|
198062
|
+
"why",
|
|
198063
|
+
"per",
|
|
198064
|
+
"via",
|
|
198065
|
+
"also"
|
|
198066
|
+
]);
|
|
198067
|
+
function tokenize(text4, drop) {
|
|
198068
|
+
const lowered = text4.toLowerCase().replace(/[`*_~#>]/g, " ");
|
|
198069
|
+
const out = [];
|
|
198070
|
+
for (const raw of lowered.split(/[^a-z0-9]+/)) {
|
|
198071
|
+
if (raw.length < 2)
|
|
198072
|
+
continue;
|
|
198073
|
+
if (STOPWORDS.has(raw))
|
|
198074
|
+
continue;
|
|
198075
|
+
if (drop.has(raw))
|
|
198076
|
+
continue;
|
|
198077
|
+
out.push(raw);
|
|
198078
|
+
}
|
|
198079
|
+
return out;
|
|
198080
|
+
}
|
|
198081
|
+
function pathWords(p) {
|
|
198082
|
+
return p.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
|
|
198083
|
+
}
|
|
198084
|
+
var HEADING_RE = /^ {0,3}#{1,6}\s+(.*)$/;
|
|
198085
|
+
function splitDocSections(body, drop) {
|
|
198086
|
+
const raws = [];
|
|
198087
|
+
let cur = { heading: "", text: "" };
|
|
198088
|
+
for (const line of body.split(/\r?\n/)) {
|
|
198089
|
+
const m = HEADING_RE.exec(line);
|
|
198090
|
+
if (m) {
|
|
198091
|
+
if (cur.text.trim() || cur.heading)
|
|
198092
|
+
raws.push(cur);
|
|
198093
|
+
cur = { heading: m[1].trim(), text: `${line}
|
|
198094
|
+
` };
|
|
198095
|
+
} else {
|
|
198096
|
+
cur.text += `${line}
|
|
198097
|
+
`;
|
|
198098
|
+
}
|
|
198099
|
+
}
|
|
198100
|
+
if (cur.text.trim() || cur.heading)
|
|
198101
|
+
raws.push(cur);
|
|
198102
|
+
return raws.map((r) => ({
|
|
198103
|
+
realHeading: r.heading === "" ? null : r.heading,
|
|
198104
|
+
tokens: new Set(tokenize(r.text, drop)),
|
|
198105
|
+
text: r.text
|
|
198106
|
+
}));
|
|
198107
|
+
}
|
|
198108
|
+
function headingKey(h) {
|
|
198109
|
+
return h.replace(/[`*_~]/g, "").trim().toLowerCase();
|
|
198110
|
+
}
|
|
198111
|
+
function normalizeForQuote(text4) {
|
|
198112
|
+
return text4.replace(/[`*_~]/g, "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
198113
|
+
}
|
|
198114
|
+
function locateQuote(sections, quote) {
|
|
198115
|
+
const needle = normalizeForQuote(quote);
|
|
198116
|
+
if (!needle)
|
|
198117
|
+
return [];
|
|
198118
|
+
const hits = [];
|
|
198119
|
+
for (let i = 0; i < sections.length; i++) {
|
|
198120
|
+
if (normalizeForQuote(sections[i].text).includes(needle))
|
|
198121
|
+
hits.push(i);
|
|
198122
|
+
}
|
|
198123
|
+
return hits;
|
|
198124
|
+
}
|
|
198125
|
+
function scoreSections(sections, noteTokens) {
|
|
198126
|
+
const N = sections.length;
|
|
198127
|
+
const df = /* @__PURE__ */ new Map();
|
|
198128
|
+
for (const s of sections)
|
|
198129
|
+
for (const t of s.tokens)
|
|
198130
|
+
df.set(t, (df.get(t) ?? 0) + 1);
|
|
198131
|
+
const idf = (t) => {
|
|
198132
|
+
const d = df.get(t) ?? 0;
|
|
198133
|
+
if (d === 0)
|
|
198134
|
+
return 0;
|
|
198135
|
+
return Math.max(0, Math.log2((N + 1) / (d + 1)));
|
|
198136
|
+
};
|
|
198137
|
+
return sections.map((s) => {
|
|
198138
|
+
let score = 0;
|
|
198139
|
+
for (const t of noteTokens)
|
|
198140
|
+
if (s.tokens.has(t))
|
|
198141
|
+
score += idf(t);
|
|
198142
|
+
return score;
|
|
198143
|
+
});
|
|
198144
|
+
}
|
|
198145
|
+
function verifyOverlapSections(input) {
|
|
198146
|
+
const { docs, note, sections, bodyOf } = input;
|
|
198147
|
+
if (sections.length === 0)
|
|
198148
|
+
return [...sections];
|
|
198149
|
+
const drop = /* @__PURE__ */ new Set([...pathWords(docs[0]), ...pathWords(docs[1])]);
|
|
198150
|
+
const noteTokens = new Set(tokenize(note, drop));
|
|
198151
|
+
return sections.map((ptr) => {
|
|
198152
|
+
const body = bodyOf(ptr.doc);
|
|
198153
|
+
if (body === void 0)
|
|
198154
|
+
return { ...ptr };
|
|
198155
|
+
const candidates = splitDocSections(body, drop);
|
|
198156
|
+
if (candidates.length === 0)
|
|
198157
|
+
return { ...ptr };
|
|
198158
|
+
let pointedIdx;
|
|
198159
|
+
if (ptr.heading === null) {
|
|
198160
|
+
pointedIdx = 0;
|
|
198161
|
+
} else {
|
|
198162
|
+
const key4 = headingKey(ptr.heading);
|
|
198163
|
+
pointedIdx = candidates.findIndex((c) => c.realHeading !== null && headingKey(c.realHeading) === key4);
|
|
198164
|
+
}
|
|
198165
|
+
if (ptr.quote !== void 0) {
|
|
198166
|
+
const hits = locateQuote(candidates, ptr.quote);
|
|
198167
|
+
if (hits.length > 0) {
|
|
198168
|
+
if (pointedIdx >= 0 && hits.includes(pointedIdx))
|
|
198169
|
+
return { ...ptr };
|
|
198170
|
+
const target = hits[0];
|
|
198171
|
+
return { ...ptr, heading: target === 0 ? null : candidates[target].realHeading };
|
|
198172
|
+
}
|
|
198173
|
+
}
|
|
198174
|
+
if (noteTokens.size === 0)
|
|
198175
|
+
return { ...ptr };
|
|
198176
|
+
const scores = scoreSections(candidates, noteTokens);
|
|
198177
|
+
let bestIdx = 0;
|
|
198178
|
+
for (let i = 1; i < scores.length; i++)
|
|
198179
|
+
if (scores[i] > scores[bestIdx])
|
|
198180
|
+
bestIdx = i;
|
|
198181
|
+
const bestScore = scores[bestIdx];
|
|
198182
|
+
const pointedScore = pointedIdx >= 0 ? scores[pointedIdx] : 0;
|
|
198183
|
+
const reanchor = bestIdx !== pointedIdx && bestScore >= MIN_MEANINGFUL_SCORE && pointedScore < MIN_MEANINGFUL_SCORE && pointedScore <= NEGLIGIBLE_RATIO * bestScore;
|
|
198184
|
+
if (!reanchor)
|
|
198185
|
+
return { ...ptr };
|
|
198186
|
+
return { ...ptr, heading: bestIdx === 0 ? null : candidates[bestIdx].realHeading };
|
|
198187
|
+
});
|
|
198188
|
+
}
|
|
198189
|
+
|
|
198190
|
+
// packages/spec-consolidator/dist/overlap-detector.js
|
|
197950
198191
|
var DEFAULT_MAX_PAIRS_PER_AREA = 60;
|
|
197951
198192
|
async function flagOverlaps(repoRoot, areas, docs, opts = {}) {
|
|
197952
198193
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -198036,6 +198277,19 @@ async function flagOverlaps(repoRoot, areas, docs, opts = {}) {
|
|
|
198036
198277
|
};
|
|
198037
198278
|
launch();
|
|
198038
198279
|
});
|
|
198280
|
+
for (const list of result.values()) {
|
|
198281
|
+
for (const overlap of list) {
|
|
198282
|
+
overlap.sections = verifyOverlapSections({
|
|
198283
|
+
docs: overlap.docs,
|
|
198284
|
+
note: overlap.note,
|
|
198285
|
+
sections: overlap.sections,
|
|
198286
|
+
bodyOf: (ref) => {
|
|
198287
|
+
const d = byPath.get(ref);
|
|
198288
|
+
return d ? docBody2(d) : void 0;
|
|
198289
|
+
}
|
|
198290
|
+
});
|
|
198291
|
+
}
|
|
198292
|
+
}
|
|
198039
198293
|
const entries = [...result].flatMap(([area, list]) => list.map((overlap) => ({ area, overlap })));
|
|
198040
198294
|
const merged = dedupeCrossAreaOverlaps(entries);
|
|
198041
198295
|
result.clear();
|
|
@@ -198090,9 +198344,12 @@ NOT a disagreement (do NOT flag):
|
|
|
198090
198344
|
|
|
198091
198345
|
Bias: when there is a PLAUSIBLE contradiction a human should check, flag it. When the docs are clearly complementary or agree, do not.
|
|
198092
198346
|
|
|
198093
|
-
When you flag an overlap,
|
|
198347
|
+
When you flag an overlap, point at WHERE each side's disputed claim lives. Below each doc you are given a CLOSED list of that doc's section options \u2014 its headings, plus a "lead" option. Do NOT recall or guess a heading; SELECT from the list. For EACH side output a pointer with two fields:
|
|
198348
|
+
- \`heading\`: EXACTLY one of that doc's listed section headings, copied verbatim \u2014 OR the JSON literal \`null\` (not the string "null", not "") for the LEAD, the text ABOVE its first heading (or, when the doc opens straight with a title, that opening title block). Never emit a heading that is not one of the listed options.
|
|
198349
|
+
- \`quote\`: a SHORT verbatim excerpt (\u2264 25 words) of the disputed sentence, copied EXACTLY from that doc \u2014 the words that state the claim in dispute. This is your evidence for the heading you picked; copy it, do not paraphrase.
|
|
198350
|
+
List one entry per side; omit a side only when it genuinely has no conflicting passage.
|
|
198094
198351
|
|
|
198095
|
-
PREAMBLE:
|
|
198352
|
+
PREAMBLE: use \`heading\`: \`null\` ONLY for that lead/preamble block; whenever the disputed passage is under a listed heading, select that heading verbatim.
|
|
198096
198353
|
|
|
198097
198354
|
In the NOTE, refer to each doc by its FILENAME (the basename shown in the header, e.g. \`users.md\`) \u2014 NEVER "doc A" / "doc B", which mean nothing to the reader.
|
|
198098
198355
|
|
|
@@ -198100,13 +198357,19 @@ Output ONLY a JSON object, no prose, no code fences:
|
|
|
198100
198357
|
|
|
198101
198358
|
{ "overlap": true,
|
|
198102
198359
|
"note": "users.md uses auth0_id; identity.md uses auth0_sub for the same user column",
|
|
198103
|
-
"sections": [
|
|
198360
|
+
"sections": [
|
|
198361
|
+
{ "side": "A", "heading": "User model", "quote": "the auth0_id column stores the Auth0 subject" },
|
|
198362
|
+
{ "side": "B", "heading": "Identity", "quote": "we persist the Auth0 subject in auth0_sub" }
|
|
198363
|
+
] }
|
|
198104
198364
|
|
|
198105
|
-
Preamble example \u2014 doc A's claim is a README
|
|
198365
|
+
Preamble example \u2014 doc A's disputed claim is a README tagline ABOVE its first heading, so its side uses \`heading\`: null and quotes that tagline verbatim:
|
|
198106
198366
|
|
|
198107
198367
|
{ "overlap": true,
|
|
198108
198368
|
"note": "README.md lists C# as a supported language in the preamble; plan.md's Tech Stack omits it",
|
|
198109
|
-
"sections": [
|
|
198369
|
+
"sections": [
|
|
198370
|
+
{ "side": "A", "heading": null, "quote": "Supports TypeScript, Python, and C# out of the box" },
|
|
198371
|
+
{ "side": "B", "heading": "Tech Stack", "quote": "The stack is TypeScript and Python" }
|
|
198372
|
+
] }
|
|
198110
198373
|
|
|
198111
198374
|
Use { "overlap": false, "note": "", "sections": [] } when they are complementary or agree. The note is shown to the user \u2014 name the specific thing that differs.`;
|
|
198112
198375
|
var OVERLAP_PREVIEW_LINES = 120;
|
|
@@ -198140,31 +198403,59 @@ function hasConcernHeading(doc, concern, vocab) {
|
|
|
198140
198403
|
}
|
|
198141
198404
|
return false;
|
|
198142
198405
|
}
|
|
198406
|
+
function sectionHeadings(body) {
|
|
198407
|
+
const out = [];
|
|
198408
|
+
for (const line of body.split(/\r?\n/)) {
|
|
198409
|
+
const m = /^ {0,3}#{1,6}\s+(.*)$/.exec(line);
|
|
198410
|
+
if (!m)
|
|
198411
|
+
continue;
|
|
198412
|
+
const text4 = m[1].replace(/\s*#*\s*$/, "").trim();
|
|
198413
|
+
if (text4)
|
|
198414
|
+
out.push(text4);
|
|
198415
|
+
}
|
|
198416
|
+
return out;
|
|
198417
|
+
}
|
|
198418
|
+
function sectionOptions(slice5) {
|
|
198419
|
+
const lines = [" - the lead (text above the first heading, or the opening title block) \u2192 use heading: null"];
|
|
198420
|
+
for (const h of sectionHeadings(slice5))
|
|
198421
|
+
lines.push(` - ${h}`);
|
|
198422
|
+
return lines;
|
|
198423
|
+
}
|
|
198143
198424
|
function buildOverlapUserPrompt(areaId, a, b) {
|
|
198144
198425
|
const slice5 = (d) => docBody2(d).split(/\r?\n/).slice(0, OVERLAP_PREVIEW_LINES).join("\n");
|
|
198426
|
+
const sliceA = slice5(a);
|
|
198427
|
+
const sliceB = slice5(b);
|
|
198145
198428
|
return [
|
|
198146
198429
|
`Area: ${areaId}`,
|
|
198147
198430
|
"",
|
|
198148
198431
|
`--- doc A: ${a.path} ---`,
|
|
198149
|
-
|
|
198432
|
+
sliceA,
|
|
198150
198433
|
`--- end doc A ---`,
|
|
198151
198434
|
"",
|
|
198152
198435
|
`--- doc B: ${b.path} ---`,
|
|
198153
|
-
|
|
198436
|
+
sliceB,
|
|
198154
198437
|
`--- end doc B ---`,
|
|
198155
198438
|
"",
|
|
198439
|
+
"SECTION OPTIONS \u2014 each side pointer MUST be one of these (verbatim), or the lead (heading: null):",
|
|
198440
|
+
"",
|
|
198441
|
+
`doc A (${a.path}):`,
|
|
198442
|
+
...sectionOptions(sliceA),
|
|
198443
|
+
"",
|
|
198444
|
+
`doc B (${b.path}):`,
|
|
198445
|
+
...sectionOptions(sliceB),
|
|
198446
|
+
"",
|
|
198156
198447
|
"Return the JSON object as specified."
|
|
198157
198448
|
].join("\n");
|
|
198158
198449
|
}
|
|
198159
198450
|
var LlmOverlapSchema = external_exports.object({
|
|
198160
198451
|
overlap: external_exports.boolean(),
|
|
198161
198452
|
note: external_exports.string().default(""),
|
|
198162
|
-
sections: external_exports.array(external_exports.object({ side: external_exports.enum(["A", "B"]), heading: external_exports.string().nullable() })).default([])
|
|
198453
|
+
sections: external_exports.array(external_exports.object({ side: external_exports.enum(["A", "B"]), heading: external_exports.string().nullable(), quote: external_exports.string().optional() })).default([])
|
|
198163
198454
|
});
|
|
198164
198455
|
var OverlapVerdictSchema = external_exports.object({
|
|
198165
198456
|
overlap: external_exports.boolean(),
|
|
198166
198457
|
note: external_exports.string().default(""),
|
|
198167
|
-
sections: external_exports.array(external_exports.object({ doc: external_exports.string(), heading: external_exports.string().nullable() })).default([])
|
|
198458
|
+
sections: external_exports.array(external_exports.object({ doc: external_exports.string(), heading: external_exports.string().nullable(), quote: external_exports.string().optional() })).default([])
|
|
198168
198459
|
});
|
|
198169
198460
|
function spawnOverlapRunner(opts = {}) {
|
|
198170
198461
|
const transport = opts.transport ?? cliTransport({ bin: opts.bin });
|
|
@@ -198184,7 +198475,11 @@ function spawnOverlapRunner(opts = {}) {
|
|
|
198184
198475
|
return {
|
|
198185
198476
|
overlap: inner.overlap,
|
|
198186
198477
|
note: inner.note,
|
|
198187
|
-
sections: inner.sections.map((s) => ({
|
|
198478
|
+
sections: inner.sections.map((s) => ({
|
|
198479
|
+
doc: s.side === "A" ? a.path : b.path,
|
|
198480
|
+
heading: s.heading,
|
|
198481
|
+
...s.quote !== void 0 ? { quote: s.quote } : {}
|
|
198482
|
+
}))
|
|
198188
198483
|
};
|
|
198189
198484
|
};
|
|
198190
198485
|
}
|
|
@@ -232441,7 +232736,7 @@ function readToolVersion() {
|
|
|
232441
232736
|
if (cachedVersion)
|
|
232442
232737
|
return cachedVersion;
|
|
232443
232738
|
if (true) {
|
|
232444
|
-
cachedVersion = "0.7.0-next.
|
|
232739
|
+
cachedVersion = "0.7.0-next.8";
|
|
232445
232740
|
return cachedVersion;
|
|
232446
232741
|
}
|
|
232447
232742
|
try {
|