@polymorphism-tech/morph-spec 8.36.0 → 8.37.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.
Files changed (32) hide show
  1. package/CLAUDE.md +5 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +2 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/docs/CLI.md +47 -24
  8. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  9. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  10. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  11. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  12. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  13. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  14. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  15. package/framework/skills/core/morph-media/SKILL.md +1 -1
  16. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  17. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  18. package/framework/skills/core/morph-review/SKILL.md +2 -2
  19. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  20. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  21. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  22. package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -3
  23. package/framework/templates/dotnet/ai-kit/README.md +12 -7
  24. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +12 -6
  25. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +16 -11
  26. package/package.json +1 -1
  27. package/src/commands/validation/verify.js +21 -8
  28. package/src/lib/tasks/dotnet-test-introspection.js +134 -19
  29. package/src/lib/tasks/eval-node.js +3 -3
  30. package/src/lib/tasks/test-runner.js +88 -15
  31. package/src/lib/tasks/test-scope.js +47 -17
  32. package/src/lib/validators/verify-runner.js +46 -15
@@ -24,14 +24,14 @@
24
24
  */
25
25
 
26
26
  import { execFileSync } from 'child_process';
27
- import { existsSync, readFileSync } from 'fs';
27
+ import { existsSync, readFileSync, rmSync } from 'fs';
28
28
  import { join, resolve } from 'path';
29
29
 
30
30
  import { runBuildCheck, getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
31
31
  import { runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
32
32
  import { buildComposeEnv } from '../e2e/compose-env.js';
33
33
  import {
34
- runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout, isZeroTestRun,
34
+ runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout, isZeroTestRun, mtpSummaryTotals,
35
35
  } from '../tasks/test-runner.js';
36
36
  import {
37
37
  resolveTestNodes, parseTestFilter, nodeMatchesFilter, recordTestBaselines, morphConfigPath,
@@ -56,6 +56,7 @@ const OUTPUT_TAIL_LINES = 20;
56
56
 
57
57
  /** A narrowed run that proved ZERO tests ran (#87) — wording pinned by the plan. */
58
58
  const NARROWED_ZERO_TESTS = 'narrowed-zero-tests: o recorte executou 0 testes';
59
+ const NARROWED_ZERO_TESTS_LINK = 'narrowed-zero-tests: um link do recorte executou 0 testes (bloco MTP com total: 0)';
59
60
 
60
61
  /** A narrowed run that exited green with no count anyone could read (#87): null is UNKNOWN, and unknown is never green. */
61
62
  const NARROWED_COUNT_UNKNOWN = 'narrowed-count-unknown: o recorte saiu verde sem contagem legível — '
@@ -469,14 +470,27 @@ async function runOneTestNode({
469
470
  const evalChannel = node.runner === EVAL_RUNNER
470
471
  ? evalNodeEnv(node, projectPath, { refresh: refreshCache })
471
472
  : null;
473
+ // The summary path is fixed per node id and pid, and nothing else removes the
474
+ // file: one left there by an earlier run (an earlier verify in the same
475
+ // process, a recycled pid) was read as THIS run's, handing a harness that
476
+ // wrote nothing the old scenario count — and hiding an empty run behind it.
477
+ if (evalChannel) discardFile(evalChannel.summaryPath);
472
478
 
473
479
  const start = Date.now();
474
480
  const result = await _runTestSuite(projectPath, {
475
481
  timeout, node, ...(evalChannel ? { env: evalChannel.env } : {}),
476
482
  });
477
483
  const entry = judgeRun({ node, result, durationMs: Date.now() - start });
484
+ if (!evalChannel) return entry;
478
485
 
479
- return evalChannel ? judgeEvalRun(entry, node, evalChannel.summaryPath) : entry;
486
+ const judged = judgeEvalRun(entry, node, evalChannel.summaryPath);
487
+ discardFile(evalChannel.summaryPath);
488
+ return judged;
489
+ }
490
+
491
+ /** Remove a scratch file the CLI owns; a file that is not there (or will not go) changes nothing. */
492
+ function discardFile(path) {
493
+ try { rmSync(path, { force: true }); } catch { /* best effort — the next run removes it first */ }
480
494
  }
481
495
 
482
496
  /**
@@ -484,10 +498,12 @@ async function runOneTestNode({
484
498
  * code said.
485
499
  *
486
500
  * Two things the exit code cannot see:
487
- * - the scenario COUNT, which is what `expectedCount` is compared against.
488
- * The summary wins when readable; otherwise the `dotnet` extractor's number
489
- * stands, because an eval IS `dotnet test`. **null stays UNKNOWN, never 0** —
490
- * an unreadable summary must not manufacture a count drop;
501
+ * - the scenario COUNT, which is what `expectedCount` is compared against and
502
+ * what `--record-baseline` writes. Read ONLY from the summary: the `dotnet`
503
+ * extractor counts xunit cases, another unit — compared with (or recorded as)
504
+ * a scenario baseline it failed green runs as drops (#110, #117 review). No
505
+ * readable summary → **null, UNKNOWN, never 0** — an unreadable summary must
506
+ * not manufacture a count drop;
491
507
  * - a suite that evaluated ZERO scenarios and exited 0. Nothing failed, because
492
508
  * nothing ran: the guardrail-inert shape this repository has already paid to
493
509
  * learn to detect.
@@ -499,8 +515,7 @@ function judgeEvalRun(entry, node, summaryPath) {
499
515
  const cold = evalColdCacheWarning(summary);
500
516
  if (cold) warnings.push(cold);
501
517
 
502
- const fromSummary = evalScenarioCount(summary);
503
- const count = fromSummary ?? entry.count;
518
+ const count = evalScenarioCount(summary);
504
519
  const verdict = compareTestCount(count, node.expectedCount);
505
520
  if (verdict.warning) warnings.push(verdict.warning);
506
521
 
@@ -645,7 +660,12 @@ function judgeRun({ node, result, durationMs }) {
645
660
  return judgeNarrowedRun({ entry, node, result, durationMs, count });
646
661
  }
647
662
 
648
- const verdict = compareTestCount(count, node.expectedCount);
663
+ // An eval node's unit is the SCENARIO (#110 part 2): its baseline is the
664
+ // summary's scenario count, and `judgeEvalRun` is the one that compares it.
665
+ // Comparing here too, with the extractor's count of xunit cases, failed a
666
+ // 24/24 run as "esperado 24, executado 2" — and `judgeEvalRun` only turns a
667
+ // pass into a fail, never the reverse.
668
+ const verdict = compareTestCount(count, node.runner === EVAL_RUNNER ? null : node.expectedCount);
649
669
  const warnings = verdict.warning ? [verdict.warning] : [];
650
670
 
651
671
  // A drop is the one count verdict that fails a node by itself: tests that
@@ -689,16 +709,24 @@ function judgeRun({ node, result, durationMs }) {
689
709
  * must never buy. What counts as proof is the selection's `zeroGuard`
690
710
  * (test-scope.js):
691
711
  * `count` only a parsed count > 0. **null is UNKNOWN, never green**;
692
- * `mtp-exit-8` MTP exits 8 on zero tests, so exit 0 IS the proof.
693
- * A parsed 0 is refused under either guard: it is positive evidence.
712
+ * `mtp-exit-8` a parsed count > 0 proves it (the MTP summary is read in
713
+ * pt-BR and en-US, #115); with no readable summary (another
714
+ * locale), exit 0 does — MTP exits 8 on zero tests.
715
+ * A parsed 0 is refused under either guard: it is positive evidence. It is
716
+ * also what closes limit (f): with exit 8 IGNORED, MTP exits 0 over a
717
+ * summary that says `total: 0` (measured, mtp-ignore-exit-code-8-zero-tests.txt).
718
+ * In a narrowed `&&` chain the SUM would hide that link behind another's
719
+ * count, so under `mtp-exit-8` every MTP block is checked on its own — MTP
720
+ * prints one block per invocation (measured), and a `total: 0` block is red.
694
721
  *
695
722
  * A FAILED run is renamed `narrowed-zero-tests (…)` only when it carries the
696
723
  * runner's zero-test signature AND no count proves tests ran. A multi-project
697
724
  * VSTest output may print "No test matches…" for one project while another
698
725
  * ran and failed; that failure is the lead, never a zero. Anything else,
699
- * exit 9 included, keeps the executor's own raw reason: projects that impose
700
- * `--minimum-expected-tests` are never narrowed, so a 9 here is not ours to
701
- * reinterpret.
726
+ * exit 9 included, keeps the executor's own raw reason: a project that imposes
727
+ * `--minimum-expected-tests` is narrowed only with its floor lowered to 1 by
728
+ * `-p:` (#114, test-scope.js), where a run with tests cannot exit 9 — so a 9
729
+ * here is not ours to reinterpret.
702
730
  *
703
731
  * Every red leads its output with the verdict, like a timeout does: the
704
732
  * feedback line reads `output` before `reason`, and a tail alone would bury it.
@@ -712,6 +740,9 @@ function judgeNarrowedRun({ entry, node, result, durationMs, count }) {
712
740
 
713
741
  if (result.passed) {
714
742
  if (count === 0) return red(NARROWED_ZERO_TESTS);
743
+ if (entry.selection.zeroGuard === 'mtp-exit-8' && mtpSummaryTotals(result.output).includes(0)) {
744
+ return red(NARROWED_ZERO_TESTS_LINK);
745
+ }
715
746
  if (ranTests) return green();
716
747
  if (entry.selection.zeroGuard === 'mtp-exit-8' && result.exitCode === 0) return green();
717
748
  return red(NARROWED_COUNT_UNKNOWN);