@rsaglobaltech/specgate 0.26.0 → 0.27.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 (58) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +6 -0
  3. package/dist/packages/core/src/domain/ChangeMeasures.js +198 -0
  4. package/dist/packages/core/src/domain/ChangeMeasures.js.map +1 -0
  5. package/dist/packages/core/src/domain/ConventionalCommit.js +73 -0
  6. package/dist/packages/core/src/domain/ConventionalCommit.js.map +1 -0
  7. package/dist/packages/core/src/domain/HarnessConfig.js.map +1 -1
  8. package/dist/packages/core/src/domain/HarnessReport.js +2 -0
  9. package/dist/packages/core/src/domain/HarnessReport.js.map +1 -1
  10. package/dist/packages/core/src/domain/QualityChecks.js +218 -0
  11. package/dist/packages/core/src/domain/QualityChecks.js.map +1 -0
  12. package/dist/packages/core/src/domain/QualitySummary.js +58 -0
  13. package/dist/packages/core/src/domain/QualitySummary.js.map +1 -0
  14. package/dist/packages/core/src/domain/Sarif.js +121 -0
  15. package/dist/packages/core/src/domain/Sarif.js.map +1 -0
  16. package/dist/packages/core/src/infrastructure/GitContributionStager.js +1 -1
  17. package/dist/packages/core/src/infrastructure/GitContributionStager.js.map +1 -1
  18. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +21 -0
  19. package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
  20. package/dist/packages/core/src/infrastructure/QualityBaselineFile.js +67 -0
  21. package/dist/packages/core/src/infrastructure/QualityBaselineFile.js.map +1 -0
  22. package/dist/scripts/cli/commands/change/TicketCommand.js +1 -1
  23. package/dist/scripts/cli/commands/change/TicketCommand.js.map +1 -1
  24. package/dist/scripts/cli/commands/harness/RunCommand.js +212 -15
  25. package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
  26. package/dist/scripts/cli/commands/quality/CheckCommand.js +54 -3
  27. package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
  28. package/dist/scripts/cli/commands/quality/QualityCommand.js +314 -0
  29. package/dist/scripts/cli/commands/quality/QualityCommand.js.map +1 -0
  30. package/dist/scripts/cli/commands/spec/DoneCommand.js +4 -1
  31. package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
  32. package/dist/scripts/cli/commands/spec/StudioCommand.js +14 -0
  33. package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
  34. package/dist/scripts/docs/nav.js +13 -2
  35. package/dist/scripts/docs/nav.js.map +1 -1
  36. package/dist/scripts/harness/prompt.js +4 -0
  37. package/dist/scripts/harness/prompt.js.map +1 -1
  38. package/dist/scripts/harness/report.js +13 -0
  39. package/dist/scripts/harness/report.js.map +1 -1
  40. package/dist/scripts/lib/quality-presets.js +378 -0
  41. package/dist/scripts/lib/quality-presets.js.map +1 -0
  42. package/dist/scripts/lib/quality-run.js +579 -0
  43. package/dist/scripts/lib/quality-run.js.map +1 -0
  44. package/dist/scripts/lib/sonar-local.js +308 -0
  45. package/dist/scripts/lib/sonar-local.js.map +1 -0
  46. package/dist/scripts/lib/studio-app-page.js +74 -11
  47. package/dist/scripts/lib/studio-app-page.js.map +1 -1
  48. package/dist/scripts/lib/studio-app.js +80 -0
  49. package/dist/scripts/lib/studio-app.js.map +1 -1
  50. package/dist/scripts/lib/studio-route.js +64 -1
  51. package/dist/scripts/lib/studio-route.js.map +1 -1
  52. package/dist/scripts/lib/surface.js +11 -0
  53. package/dist/scripts/lib/surface.js.map +1 -1
  54. package/dist/scripts/lib/ticket-keys.js +1 -1
  55. package/dist/scripts/lib/ticket-keys.js.map +1 -1
  56. package/dist/scripts/quality.js +10 -0
  57. package/dist/scripts/quality.js.map +1 -0
  58. package/package.json +1 -1
@@ -84,6 +84,7 @@ const harness_events_1 = require("../../../lib/harness-events");
84
84
  const clarification_1 = require("../../../lib/clarification");
85
85
  const screen_requirements_1 = require("../../../lib/screen-requirements");
86
86
  const harness_guard_1 = require("../../../lib/harness-guard");
87
+ const quality_run_1 = require("../../../lib/quality-run");
87
88
  const HarnessEvents_1 = require("../../../../packages/core/src/domain/HarnessEvents");
88
89
  /**
89
90
  * `read` against the project as it will be after its open changes are
@@ -497,6 +498,7 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
497
498
  // The agent's tests are new evidence for a generated matrix: refresh it so
498
499
  // the gate judges the attempt, not the matrix as it was before it.
499
500
  (0, MatrixCommand_1.refreshDerivedMatrix)(worktreeDir);
501
+ let passHint = "";
500
502
  const validate = judged(worktreeDir, (dir, toProject) => {
501
503
  const validateArgs = [VALIDATE_SCRIPT, dir, "--strict"];
502
504
  if (req.requirement)
@@ -537,13 +539,16 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
537
539
  // and its failure goes back to the agent like any other gate failure.
538
540
  const archCmd = String(settings.archCmd || "").trim();
539
541
  if (archCmd) {
540
- const arch = (0, node_child_process_1.spawnSync)(archCmd, {
541
- shell: true,
542
- cwd: worktreeDir,
543
- encoding: "utf8",
544
- timeout: timeoutMs,
545
- maxBuffer: SUBPROCESS_MAX_BUFFER,
546
- });
542
+ // REQ-231: with arch_sarif, only a violation this attempt adds fails it.
543
+ const arch = settings.archSarif
544
+ ? (0, quality_run_1.archRun)(worktreeDir, archCmd, settings.archSarif, "", req.requirement || "")
545
+ : (0, node_child_process_1.spawnSync)(archCmd, {
546
+ shell: true,
547
+ cwd: worktreeDir,
548
+ encoding: "utf8",
549
+ timeout: timeoutMs,
550
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
551
+ });
547
552
  report(`architecture check: ${archCmd}`, arch.status === 0 ? "pass" : "fail");
548
553
  if (arch.status !== 0) {
549
554
  return {
@@ -625,12 +630,52 @@ function runGate(worktreeDir, testCmd, timeoutMs, req = {}, settings = {}, repor
625
630
  errors.map((e) => ` ${e.message}\n fix: ${e.fix}`).join("\n\n"),
626
631
  };
627
632
  }
628
- if (warnings.length > 0) {
629
- return { ok: true, stage: "", output: "", hint: warnings[0].message };
633
+ if (warnings.length > 0)
634
+ passHint = warnings[0].message;
635
+ }
636
+ }
637
+ // REQ-220, REQ-221: the quality checks, after the tests — the attempt ones,
638
+ // then the end ones. A failure goes back to the agent like a failing test.
639
+ const quality = settings.quality;
640
+ if (quality && quality.checks.length > 0) {
641
+ const ctx = (0, quality_run_1.gateContext)(worktreeDir, req.requirement || "", "");
642
+ const outcomes = [];
643
+ for (const stage of ["attempt", "end"]) {
644
+ for (const o of (0, quality_run_1.runStages)(worktreeDir, quality, [stage], ctx, undefined, agentChecks(worktreeDir, req, settings, timeoutMs, testCmd))) {
645
+ outcomes.push(o);
646
+ // REQ-229: the step says what was measured, so /live shows it as it happens.
647
+ const said = o.skipped
648
+ ? " (not run)"
649
+ : o.measure && o.measure.value !== null
650
+ ? ` ${o.measure.value}%`
651
+ : o.findings
652
+ ? ` · ${o.findings.freshCount} new`
653
+ : "";
654
+ report(`quality: ${o.name}${said}`, o.ok || !!o.skipped || !o.gate ? "pass" : "fail", {
655
+ ms: o.ms,
656
+ ...(o.skipped ? { skipped: o.skipped } : {}),
657
+ ...(o.measure ? { value: o.measure.value } : {}),
658
+ });
659
+ if (o.gate && !o.ok && !o.skipped) {
660
+ return {
661
+ ok: false,
662
+ stage: `quality check: ${o.name}`,
663
+ output: o.output,
664
+ hint: o.measure && o.measure.kind === "coverage"
665
+ ? "Write the tests that run the lines named above; do not lower the threshold."
666
+ : o.measure && o.measure.kind === "mutation"
667
+ ? "Strengthen the tests so each surviving mutant named above would fail one."
668
+ : o.name === "commits"
669
+ ? "Reword the commits named above as Conventional Commits, or do not commit: the harness commits for you."
670
+ : "Fix what the check found. Do not silence it: no @SuppressWarnings, eslint-disable or baseline edit.",
671
+ checks: outcomes,
672
+ };
673
+ }
630
674
  }
631
675
  }
676
+ return { ok: true, stage: "", output: "", hint: passHint, checks: outcomes };
632
677
  }
633
- return { ok: true, stage: "", output: "", hint: "" };
678
+ return { ok: true, stage: "", output: "", hint: passHint };
634
679
  }
635
680
  /**
636
681
  * Undo whatever an advisory agent touched.
@@ -673,6 +718,151 @@ function discardWorktreeChanges(worktreeDir) {
673
718
  // reviewer's edits must not discard the evidence.
674
719
  git(worktreeDir, ["clean", "-fd", "-e", EmptyAttempt_1.PROMPT_ARCHIVE_DIR]);
675
720
  }
721
+ /**
722
+ * The worktree exactly as it is, to put back after an agent that must not
723
+ * change it: tracked files through `git stash create` (which touches
724
+ * nothing), untracked ones by content. An agent check runs before the commit,
725
+ * so the implementer's work is uncommitted — `discardWorktreeChanges` would
726
+ * throw it away (REQ-227).
727
+ */
728
+ function snapshotWorktree(worktreeDir) {
729
+ const stash = (git(worktreeDir, ["stash", "create"]).stdout || "").trim();
730
+ const untracked = (git(worktreeDir, ["ls-files", "--others", "--exclude-standard", "-z"]).stdout || "")
731
+ .split("\0")
732
+ .filter(Boolean);
733
+ const saved = new Map(untracked.map((rel) => [rel, fs.readFileSync(path.join(worktreeDir, rel))]));
734
+ return () => {
735
+ git(worktreeDir, ["checkout", stash || "HEAD", "--", "."]);
736
+ const now = (git(worktreeDir, ["ls-files", "--others", "--exclude-standard", "-z"]).stdout || "")
737
+ .split("\0")
738
+ .filter(Boolean);
739
+ for (const rel of now) {
740
+ if (!saved.has(rel) && !rel.startsWith(EmptyAttempt_1.PROMPT_ARCHIVE_DIR)) {
741
+ fs.rmSync(path.join(worktreeDir, rel), { force: true });
742
+ }
743
+ }
744
+ for (const [rel, content] of saved) {
745
+ fs.mkdirSync(path.dirname(path.join(worktreeDir, rel)), { recursive: true });
746
+ fs.writeFileSync(path.join(worktreeDir, rel), content);
747
+ }
748
+ };
749
+ }
750
+ /** Run an agent on an instruction built into the requirement's prompt; its output. */
751
+ function runAgentOnce(worktreeDir, command, prompt, timeoutMs, tag) {
752
+ const promptFile = path.join(os.tmpdir(), `specgate-${tag}-${crypto.randomBytes(4).toString("hex")}.md`);
753
+ try {
754
+ fs.writeFileSync(promptFile, prompt, "utf8");
755
+ const r = (0, node_child_process_1.spawnSync)((0, HarnessRun_1.substituteAgentCommand)(command, promptFile), {
756
+ shell: true,
757
+ cwd: worktreeDir,
758
+ encoding: "utf8",
759
+ timeout: timeoutMs,
760
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
761
+ stdio: ["ignore", "pipe", "pipe"],
762
+ });
763
+ return `${r.stdout || ""}${r.stderr || ""}`.trim();
764
+ }
765
+ finally {
766
+ fs.rmSync(promptFile, { force: true });
767
+ }
768
+ }
769
+ /**
770
+ * The agent checks of REQ-227, lent to the quality stages of a harness gate:
771
+ * `builtin:review` — a second agent reads the change against the requirement
772
+ * and answers VERDICT: PASS or FAIL; `builtin:adversary` — one writes a test
773
+ * that tries to break it, which is run and then removed. Neither may change
774
+ * the work: the worktree is put back exactly as it was.
775
+ */
776
+ function agentChecks(worktreeDir, req, settings, timeoutMs, testCmd) {
777
+ const commandFor = (check, fallback) => {
778
+ const profile = check.profile || fallback;
779
+ if (!profile)
780
+ return settings.agent || "";
781
+ const known = (settings.profileAgents || {})[profile];
782
+ if (known)
783
+ return known;
784
+ try {
785
+ // A profile only a check names is not resolved with the others.
786
+ return (0, HarnessConfigFile_1.resolveProfileAgent)(worktreeDir, profile);
787
+ }
788
+ catch {
789
+ return settings.agent || "";
790
+ }
791
+ };
792
+ const promptFor = (instruction) => (0, prompt_1.buildPrompt)(req, worktreeDir, {
793
+ promptPrefix: settings.promptPrefix,
794
+ hint: instruction,
795
+ withPrecedents: settings.promptPrecedents === true,
796
+ });
797
+ return {
798
+ "builtin:review": (check) => {
799
+ const command = commandFor(check, settings.reviewProfile || "");
800
+ if (!command)
801
+ return { ok: true, output: "no agent configured to review" };
802
+ const diff = (git(worktreeDir, ["diff", "HEAD"]).stdout || "").slice(0, 60000);
803
+ const restore = snapshotWorktree(worktreeDir);
804
+ let said = "";
805
+ try {
806
+ said = runAgentOnce(worktreeDir, command, promptFor(`Review only; change no file. Compare the change below with ${req.requirement}, its ` +
807
+ "statement and every scenario. Answer with a first line `VERDICT: PASS` when the " +
808
+ "change does what the requirement says and nothing it forbids, or `VERDICT: FAIL` " +
809
+ "followed by each reason as `- path:line — what is wrong`.\n\n```diff\n" +
810
+ diff +
811
+ "\n```"), timeoutMs, "review");
812
+ }
813
+ finally {
814
+ restore();
815
+ }
816
+ const verdict = /VERDICT:\s*(PASS|FAIL)/i.exec(said);
817
+ if (!verdict) {
818
+ return {
819
+ ok: true,
820
+ output: `The reviewer gave no verdict:
821
+ ${said.slice(0, 2000)}`,
822
+ };
823
+ }
824
+ return { ok: verdict[1].toUpperCase() === "PASS", output: said.slice(0, 4000) };
825
+ },
826
+ "builtin:adversary": (check) => {
827
+ const command = commandFor(check, settings.adversaryProfile || "");
828
+ if (!command || !testCmd)
829
+ return { ok: true, output: "no agent or test command to probe with" };
830
+ const restore = snapshotWorktree(worktreeDir);
831
+ try {
832
+ const before = new Set((git(worktreeDir, ["status", "--porcelain"]).stdout || "").split("\n").filter(Boolean));
833
+ const said = runAgentOnce(worktreeDir, command, promptFor(`Try to **break** the implementation of ${req.requirement}: write one additional ` +
834
+ "test, in the project's test style, for a case it should handle and might not — an " +
835
+ "empty input, a boundary, a duplicate, a failure path. Write the test and nothing " +
836
+ "else; do not modify the implementation, the specification or any existing test."), timeoutMs, "adversary");
837
+ const wrote = (git(worktreeDir, ["status", "--porcelain"]).stdout || "")
838
+ .split("\n")
839
+ .filter((l) => l && !before.has(l) && !l.slice(3).startsWith(EmptyAttempt_1.PROMPT_ARCHIVE_DIR));
840
+ if (wrote.length === 0)
841
+ return { ok: true, output: "The adversary wrote no test." };
842
+ const test = (0, node_child_process_1.spawnSync)((0, HarnessRun_1.substituteGateCommand)(testCmd, req), {
843
+ shell: true,
844
+ cwd: worktreeDir,
845
+ encoding: "utf8",
846
+ timeout: timeoutMs,
847
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
848
+ });
849
+ return {
850
+ ok: test.status === 0,
851
+ output: [
852
+ `The adversary wrote ${wrote.map((l) => l.slice(3)).join(", ")}; the tests ${test.status === 0 ? "still pass" : "fail"}.`,
853
+ said.slice(0, 1500),
854
+ test.status === 0 ? "" : `${test.stdout || ""}${test.stderr || ""}`.slice(-3000),
855
+ ]
856
+ .filter(Boolean)
857
+ .join("\n\n"),
858
+ };
859
+ }
860
+ finally {
861
+ restore();
862
+ }
863
+ },
864
+ };
865
+ }
676
866
  /**
677
867
  * Let an adversary try to break a green implementation (D3).
678
868
  *
@@ -716,7 +906,10 @@ function runAdversarialProbe(worktreeDir, req, settings, timeoutMs, promptFor) {
716
906
  // case is telling you something mildly reassuring.
717
907
  return { ran: true, broke: false, detail: "The adversary wrote no test.", wrote: [] };
718
908
  }
719
- const after = runGate(worktreeDir, settings.testCmd, timeoutMs, req, settings);
909
+ const after = runGate(worktreeDir, settings.testCmd, timeoutMs, req, {
910
+ ...settings,
911
+ quality: undefined,
912
+ });
720
913
  return {
721
914
  ran: true,
722
915
  broke: !after.ok,
@@ -1269,6 +1462,7 @@ function attemptRequirement(req, ctx) {
1269
1462
  // The probe never changes the verdict; it travels beside it so
1270
1463
  // `harness report` and the run record can show what it found.
1271
1464
  ...(probe.ran ? { adversarial: probe } : {}),
1465
+ ...(gate.checks ? { checks: gate.checks.map(({ output: _o, ...rest }) => rest) } : {}),
1272
1466
  };
1273
1467
  }
1274
1468
  // Every attempt is spent. Commit what the agent produced anyway, on the
@@ -1572,13 +1766,14 @@ function resumeFrom(worktreeDir, requirement) {
1572
1766
  return { attempt: 1, previousFailure: "" };
1573
1767
  }
1574
1768
  // Interrupted or exhausted? The two deserve different answers, and the branch
1575
- // says which: `preserveFailedAttempt` leaves a `wip(...): FAILED the gate`
1769
+ // says which: `preserveFailedAttempt` leaves a `chore(...): attempt FAILED the gate`
1576
1770
  // commit when the attempts run out, and an interruption leaves none.
1577
1771
  //
1578
1772
  // exhausted → the last attempt reached a verdict → resume at the next one
1579
1773
  // interrupted → it never did → re-run it, without charging the budget
1580
1774
  const head = git(worktreeDir, ["log", "-1", "--format=%s"]);
1581
- const lastCompleted = /^wip\([^)]*\): FAILED the gate/.test((head.stdout || "").trim());
1775
+ // `chore(…): attempt FAILED` since REQ-234; `wip(…): FAILED` on branches older versions left.
1776
+ const lastCompleted = /^(?:chore\([^)]*\): attempt|wip\([^)]*\):) FAILED the gate/.test((head.stdout || "").trim());
1582
1777
  const point = (0, ResumeState_1.resumePoint)(names, requirement, lastCompleted);
1583
1778
  if (!point.latest)
1584
1779
  return { attempt: 1, previousFailure: "" };
@@ -1599,7 +1794,7 @@ function preserveFailedAttempt(worktreeDir, req, failure) {
1599
1794
  const commit = git(worktreeDir, [
1600
1795
  "commit",
1601
1796
  "-m",
1602
- `wip(${req.requirement}): FAILED the gate — do not merge as is\n\n` +
1797
+ `chore(${req.requirement}): attempt FAILED the gate — do not merge as is\n\n` +
1603
1798
  `${firstLine}\n\n` +
1604
1799
  "Committed by `specgate harness run` so the attempt is reviewable rather than\n" +
1605
1800
  "discarded. The requirement is still Draft: `specgate apply` never ran.",
@@ -2184,7 +2379,9 @@ async function runChain(pending, ctx, hintByReq) {
2184
2379
  const eventsFile = (0, harness_events_1.eventsFileFor)(projectDir, req.requirement);
2185
2380
  const attempt = Number(result.attempts) || undefined;
2186
2381
  (0, harness_events_1.appendEvent)(eventsFile, { type: "merge", into: "HEAD", branch }, req.requirement, attempt);
2187
- const gate = runGate(projectDir, ctx.settings.testCmd, ctx.timeoutMs, req, ctx.settings, (stage, res, extra) => (0, harness_events_1.appendEvent)(eventsFile, { type: "gate", stage: `post-merge ${stage}`, result: res, ...(extra || {}) }, req.requirement, attempt));
2382
+ const gate = runGate(projectDir, ctx.settings.testCmd, ctx.timeoutMs, req,
2383
+ // The checks judged the branch already; the post-merge gate is the suite's.
2384
+ { ...ctx.settings, quality: undefined }, (stage, res, extra) => (0, harness_events_1.appendEvent)(eventsFile, { type: "gate", stage: `post-merge ${stage}`, result: res, ...(extra || {}) }, req.requirement, attempt));
2188
2385
  if (!gate.ok) {
2189
2386
  git(projectDir, ["reset", "-q", "--hard", "HEAD~1"]);
2190
2387
  Object.assign(result, {