@novedu/cli 0.16.0 → 0.17.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/main.js +56 -30
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1380,11 +1380,31 @@ async function loadYaml(url, fetchImpl, opts = {}) {
|
|
|
1380
1380
|
* Handlebars, so a literal `{{` in course material can never execute.
|
|
1381
1381
|
*/
|
|
1382
1382
|
async function assembleFragmentPrompt(block, baseUrl, fetchImpl, opts = {}, hostText = "") {
|
|
1383
|
+
const result = await assembleFragmentPrompts(block, baseUrl, fetchImpl, opts, [hostText]);
|
|
1384
|
+
if (!result.ok) return result;
|
|
1385
|
+
return {
|
|
1386
|
+
ok: true,
|
|
1387
|
+
prompt: result.prompts[0] ?? "",
|
|
1388
|
+
warnings: result.warnings
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* The multi-host variant of {@link assembleFragmentPrompt}: renders SEVERAL host texts
|
|
1393
|
+
* of the SAME document (e.g. a quiz's `instructions` and `discussion.instructions`)
|
|
1394
|
+
* against the document's one fragment block in a single pass — every fragment library
|
|
1395
|
+
* and text file is fetched and checked ONCE, placements are parsed per host but
|
|
1396
|
+
* consistency-checked in ONE pass over their union (so a library placed in any host
|
|
1397
|
+
* counts as used), and each host renders on its own. Any error fails the whole call
|
|
1398
|
+
* (fail closed); `prompts` is index-aligned with `hostTexts`. The template-semantics
|
|
1399
|
+
* opt-in applies identically: with neither list declared, ALL host texts return
|
|
1400
|
+
* byte-verbatim.
|
|
1401
|
+
*/
|
|
1402
|
+
async function assembleFragmentPrompts(block, baseUrl, fetchImpl, opts = {}, hostTexts = []) {
|
|
1383
1403
|
const warnings = [];
|
|
1384
1404
|
const allowedSchemes = opts.allowedSchemes ?? DEFAULT_ALLOWED_SCHEMES;
|
|
1385
1405
|
if (block.fragment_files.length === 0 && block.text_files.length === 0) return {
|
|
1386
1406
|
ok: true,
|
|
1387
|
-
|
|
1407
|
+
prompts: [...hostTexts],
|
|
1388
1408
|
warnings
|
|
1389
1409
|
};
|
|
1390
1410
|
const fragmentSettledPromise = Promise.all(block.fragment_files.map(async (ref) => {
|
|
@@ -1497,13 +1517,14 @@ async function assembleFragmentPrompt(block, baseUrl, fetchImpl, opts = {}, host
|
|
|
1497
1517
|
libraryErrors.push(...checked.errors);
|
|
1498
1518
|
warnings.push(...checked.warnings);
|
|
1499
1519
|
}
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1520
|
+
const parsedHosts = hostTexts.map((hostText) => parseHostPlacements(hostText));
|
|
1521
|
+
const parseErrors = parsedHosts.flatMap((parsed) => parsed.errors);
|
|
1522
|
+
if (parseErrors.length > 0) return {
|
|
1502
1523
|
ok: false,
|
|
1503
|
-
errors: [...libraryErrors, ...
|
|
1524
|
+
errors: [...libraryErrors, ...parseErrors],
|
|
1504
1525
|
warnings
|
|
1505
1526
|
};
|
|
1506
|
-
const placementCheck = checkPlacements(parsed.placements, fragmentFilesByAlias, block.fragment_files, textFilesByAlias, block.text_files, opts.validateLibraries ?? false);
|
|
1527
|
+
const placementCheck = checkPlacements(parsedHosts.flatMap((parsed) => parsed.placements), fragmentFilesByAlias, block.fragment_files, textFilesByAlias, block.text_files, opts.validateLibraries ?? false);
|
|
1507
1528
|
warnings.push(...placementCheck.warnings);
|
|
1508
1529
|
const preRenderErrors = [...libraryErrors, ...placementCheck.errors];
|
|
1509
1530
|
if (preRenderErrors.length > 0) return {
|
|
@@ -1511,28 +1532,33 @@ async function assembleFragmentPrompt(block, baseUrl, fetchImpl, opts = {}, host
|
|
|
1511
1532
|
errors: preRenderErrors,
|
|
1512
1533
|
warnings
|
|
1513
1534
|
};
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
warnings
|
|
1528
|
-
};
|
|
1535
|
+
const prompts = [];
|
|
1536
|
+
const renderErrors = [];
|
|
1537
|
+
for (const hostText of hostTexts) try {
|
|
1538
|
+
prompts.push(renderHostTemplate(hostText, (ref, args) => {
|
|
1539
|
+
const resolved = resolveAndMerge(ref, args, fragmentFilesByAlias);
|
|
1540
|
+
if (resolved.content === null || resolved.errors.length > 0) throw new Error(`Fragment "${ref}" could not be resolved`);
|
|
1541
|
+
return renderFragmentContent(resolved.content, resolved.variables);
|
|
1542
|
+
}, (alias, from, to) => {
|
|
1543
|
+
const body = textFilesByAlias.get(alias);
|
|
1544
|
+
if (body === void 0) throw new Error(`Text file "${alias}" was not prefetched`);
|
|
1545
|
+
if (from === void 0 && to === void 0) return body;
|
|
1546
|
+
return sliceLines(body, from, to);
|
|
1547
|
+
}));
|
|
1529
1548
|
} catch (e) {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
errors: [error("ASSEMBLY_ERROR", `Failed to render system prompt: ${e instanceof Error ? e.message : String(e)}`)],
|
|
1533
|
-
warnings
|
|
1534
|
-
};
|
|
1549
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1550
|
+
renderErrors.push(error("ASSEMBLY_ERROR", `Failed to render system prompt: ${message}`));
|
|
1535
1551
|
}
|
|
1552
|
+
if (renderErrors.length > 0) return {
|
|
1553
|
+
ok: false,
|
|
1554
|
+
errors: renderErrors,
|
|
1555
|
+
warnings
|
|
1556
|
+
};
|
|
1557
|
+
return {
|
|
1558
|
+
ok: true,
|
|
1559
|
+
prompts,
|
|
1560
|
+
warnings
|
|
1561
|
+
};
|
|
1536
1562
|
}
|
|
1537
1563
|
/**
|
|
1538
1564
|
* Validate a fragment FILE on its own (the `--kind fragment` / "Fragment library"
|
|
@@ -1699,7 +1725,7 @@ const QuizYamlSchema = z.strictObject({
|
|
|
1699
1725
|
id: "llm",
|
|
1700
1726
|
description: "The single model + provider that grades and discusses answers."
|
|
1701
1727
|
}),
|
|
1702
|
-
discussion: z.strictObject({ instructions: z.string().min(1).meta({ description: "Optional guidance appended to the per-question follow-up discussion chat's system prompt." }) }).optional().meta({
|
|
1728
|
+
discussion: z.strictObject({ instructions: z.string().min(1).meta({ description: "Optional guidance appended to the per-question follow-up discussion chat's system prompt. When any fragment_files or text_files are declared, place fragments inline with {{fragment \"alias.id\" …}} and embed text files with {{file \"alias\"}} (escape a literal {{ as \\{{) — same rules as instructions." }) }).optional().meta({
|
|
1703
1729
|
id: "discussion",
|
|
1704
1730
|
description: "Optional guidance for the per-question follow-up discussion chat."
|
|
1705
1731
|
}),
|
|
@@ -1825,13 +1851,13 @@ async function checkInclude(ref, baseUrl, fetchImpl, opts) {
|
|
|
1825
1851
|
errors: [includeUnreadable(ref.id, includeUrl, checked.errors)],
|
|
1826
1852
|
warnings: checked.warnings
|
|
1827
1853
|
};
|
|
1828
|
-
const assembled = await
|
|
1854
|
+
const assembled = await assembleFragmentPrompts({
|
|
1829
1855
|
fragment_files: valid.data.fragment_files,
|
|
1830
1856
|
text_files: valid.data.text_files
|
|
1831
1857
|
}, includeUrl, fetchImpl, {
|
|
1832
1858
|
allowedSchemes: opts.allowedSchemes,
|
|
1833
1859
|
validateLibraries: opts.validateLibraries ?? true
|
|
1834
|
-
}, valid.data.instructions ?? "");
|
|
1860
|
+
}, [valid.data.instructions ?? "", valid.data.discussion?.instructions ?? ""]);
|
|
1835
1861
|
if (!assembled.ok) return {
|
|
1836
1862
|
ok: false,
|
|
1837
1863
|
errors: [includeUnreadable(ref.id, includeUrl, assembled.errors)],
|
|
@@ -1868,13 +1894,13 @@ async function loadAndCheckQuiz(url, fetchImpl, opts = {}) {
|
|
|
1868
1894
|
};
|
|
1869
1895
|
const checked = checkQuizParsed(valid.data);
|
|
1870
1896
|
if (!checked.ok) return checked;
|
|
1871
|
-
const assembled = await
|
|
1897
|
+
const assembled = await assembleFragmentPrompts({
|
|
1872
1898
|
fragment_files: valid.data.fragment_files,
|
|
1873
1899
|
text_files: valid.data.text_files
|
|
1874
1900
|
}, url, fetchImpl, {
|
|
1875
1901
|
allowedSchemes: opts.allowedSchemes,
|
|
1876
1902
|
validateLibraries: opts.validateLibraries ?? true
|
|
1877
|
-
}, valid.data.instructions ?? "");
|
|
1903
|
+
}, [valid.data.instructions ?? "", valid.data.discussion?.instructions ?? ""]);
|
|
1878
1904
|
const warnings = [...checked.warnings, ...assembled.warnings];
|
|
1879
1905
|
if (!assembled.ok) return {
|
|
1880
1906
|
ok: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novedu/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Command-line companion for the Novedu chat app. Validates tutor, fragment, quiz, writing and coding YAML definitions; signs in with Entra ID and manages codes, app-hosted files and images over the app's API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|