@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.
Files changed (2) hide show
  1. package/dist/main.js +56 -30
  2. 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
- prompt: hostText,
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 parsed = parseHostPlacements(hostText);
1501
- if (parsed.errors.length > 0) return {
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, ...parsed.errors],
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
- try {
1515
- return {
1516
- ok: true,
1517
- prompt: renderHostTemplate(hostText, (ref, args) => {
1518
- const resolved = resolveAndMerge(ref, args, fragmentFilesByAlias);
1519
- if (resolved.content === null || resolved.errors.length > 0) throw new Error(`Fragment "${ref}" could not be resolved`);
1520
- return renderFragmentContent(resolved.content, resolved.variables);
1521
- }, (alias, from, to) => {
1522
- const body = textFilesByAlias.get(alias);
1523
- if (body === void 0) throw new Error(`Text file "${alias}" was not prefetched`);
1524
- if (from === void 0 && to === void 0) return body;
1525
- return sliceLines(body, from, to);
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
- return {
1531
- ok: false,
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 assembleFragmentPrompt({
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 assembleFragmentPrompt({
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.16.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": {