@tekyzinc/gsd-t 5.17.13 → 5.18.10

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.
@@ -40,6 +40,7 @@ export const meta = {
40
40
  { title: "CI-Parity", detail: "M57 build-coverage + ci-parity (FAIL-blocking)" },
41
41
  { title: "Test-Data Purge", detail: "M58 test-data --purge (FAIL-blocking)" },
42
42
  { title: "Guard-Map Gate", detail: "M87 [RULE] guard-map gate + §1.1 flow-line style gate (FAIL-blocking, §7 discovery)" },
43
+ { title: "Test-Plan Gate", detail: "M115 test-plan shape gate (testplan-lint over .gsd-t/test-plans/)" },
43
44
  { title: "Orthogonal Triad", detail: "code-review ultra ∥ Red Team ∥ QA" },
44
45
  { title: "Synthesis", detail: "merge without collapsing categories" },
45
46
  ],
@@ -143,6 +144,28 @@ function loadProtocolInstruction(name) {
143
144
  return `Read your protocol FIRST. Find it by running in Bash: \`cat "$(npm root -g)/@tekyzinc/gsd-t/${rel}"\` (or, if a project-local \`${rel}\` exists, read that instead). Follow that protocol exactly.`;
144
145
  }
145
146
 
147
+ // M115 test-plan gate — pure formatters (no branch continues past a failure; each
148
+ // reads an already-known-shape object and joins it into one display string).
149
+ //
150
+ // gsd-t-testplan-lint.cjs --dir's per-result entries carry { doc, ok, exitCode, reason }
151
+ // where "reason" is only set for a per-doc read failure (e.g. unreadable file); a
152
+ // shape violation's detail lives in the TOP-LEVEL "violations" array instead, each
153
+ // entry itself carrying its own "doc" field. So a malformed result is named from
154
+ // r.reason when present, else by filtering the top-level array down to its own doc.
155
+ function formatMalformedPlanResult(r, allViolations) {
156
+ const ownViolations = allViolations.filter((v) => v.doc === r.doc);
157
+ const kinds = ownViolations.map((v) => v.kind);
158
+ const detail = typeof r.reason === "string" && r.reason.length > 0
159
+ ? r.reason
160
+ : (kinds.length > 0 ? kinds.join(", ") : "(detail unavailable)");
161
+ const docName = typeof r.doc === "string" ? r.doc.split("/").pop() : "(doc unknown)";
162
+ return `${docName}: ${detail}`;
163
+ }
164
+ function formatMalformedPlanSummary(malformedResults, allViolations, topLevelReason) {
165
+ const parts = malformedResults.map((r) => formatMalformedPlanResult(r, allViolations));
166
+ return parts.length > 0 ? parts.join("; ") : (topLevelReason || "(violation detail unavailable)");
167
+ }
168
+
146
169
  const projectDir = _args.projectDir || ".";
147
170
  const milestone = _args.milestone || null;
148
171
  const skipUltra = _args.skipUltra || false;
@@ -798,6 +821,109 @@ if (styleGate.exitCode === 64) {
798
821
  log(`PseudoCode style gate: PASS (${styleEnv.docsChecked || 0} doc(s), ${skipped} grandfathered) — proceeding to orthogonal triad`);
799
822
  }
800
823
 
824
+ // ─────────────────────────────────────────────────────────────────────────────
825
+ // M115 Test-Plan Shape Gate (FAIL-blocking) — contract test-plan-first-contract.md §5
826
+ //
827
+ // Sibling of the guard-map/style gates above, but discovery here is DETERMINISTIC
828
+ // (a glob through runCli), not an LLM discovery agent — the task explicitly prefers
829
+ // this, and the guard-map gate's own discovery-agent `.catch(...skips...)` is the
830
+ // exact bug this gate must NOT copy: turning a broken discovery into a silent skip
831
+ // lets a real malformed plan pass verify whenever discovery hiccups.
832
+ //
833
+ // Four outcomes, kept distinct and never collapsed:
834
+ // - Plans found, all clean → PASS.
835
+ // - Plans found, any malformed → FAIL.
836
+ // - No plan exists → SKIP, named reason "no-test-plan".
837
+ // - Discovery itself failed → FAIL, named reason "testplan-discovery-error".
838
+ // Never a skip — a gate that cannot check must halt, never pass.
839
+ // ─────────────────────────────────────────────────────────────────────────────
840
+ phase("Test-Plan Gate");
841
+ const testPlanDiscovery = await runCli(
842
+ projectDir,
843
+ "testplan-lint",
844
+ ["--dir", `${projectDir}/.gsd-t/test-plans`, "--json"],
845
+ "gsd-t-testplan-lint.cjs",
846
+ "m115:testplan-discovery",
847
+ true,
848
+ "Test-Plan Gate"
849
+ );
850
+ // runCli's own .catch already sets exitCode:-1/ok:false on an agent-level error
851
+ // (the Bash call itself failing to run) — that is ALREADY a FAIL shape, never a
852
+ // skip, which is what makes "discovery failing is a HALT" the default here rather
853
+ // than something this gate has to construct.
854
+ let testPlanGateResult = null;
855
+ const testPlanEnvelopeUsable = testPlanDiscovery.exitCode !== -1 && testPlanDiscovery.envelope !== null && testPlanDiscovery.envelope !== undefined;
856
+ if (testPlanEnvelopeUsable === false) {
857
+ const why = testPlanDiscovery.exitCode === -1
858
+ ? "testplan-lint discovery did not run (agent/CLI error)"
859
+ : `testplan-lint returned unparseable output (exitCode=${testPlanDiscovery.exitCode})`;
860
+ log(`M115 test-plan gate: ${why} — halting (testplan-discovery-error), never a skip`);
861
+ testPlanGateResult = {
862
+ status: "testplan-gate-failed",
863
+ overallVerdict: "VERIFY-FAILED",
864
+ reason: `testplan-discovery-error: ${why} — a gate that cannot check must HALT, never pass`,
865
+ testPlanGate: { discovery: testPlanDiscovery.envelope, reason: "testplan-discovery-error" },
866
+ guardMap: { discovery: guardMapDiscovery, results: guardMapResults },
867
+ verifyGate: vg.envelope,
868
+ autoResearchGate: arGate,
869
+ };
870
+ } else {
871
+ const tpEnv = testPlanDiscovery.envelope;
872
+ const tpResults = Array.isArray(tpEnv.results) ? tpEnv.results : [];
873
+ // gsd-t-testplan-lint.cjs --dir reports "nothing to gate" two different ways,
874
+ // both legitimate absences rather than a broken discovery or a malformed plan:
875
+ // - the dir does not exist at all → exitCode 64, ok:false, top-level "reason"
876
+ // - the dir exists but holds no TestPlan-*.md → exitCode 0, ok:true, docsChecked:0
877
+ // Both collapse to the SAME named skip here — neither is a real shape violation.
878
+ // Only a directory that does NOT EXIST is "nothing to gate". An unreadable one
879
+ // (EACCES, EISDIR, …) is a discovery failure and must HALT — Red Team M115 MEDIUM.
880
+ const dirAbsent = testPlanDiscovery.exitCode === 64 && tpResults.length === 0 && typeof tpEnv.reason === "string" && /ENOENT/.test(tpEnv.reason);
881
+ const discoveryBroken = testPlanDiscovery.exitCode === 64 && dirAbsent === false;
882
+ const dirEmpty = tpEnv.docsChecked === 0 && tpResults.length === 0;
883
+ const noPlanToGate = dirAbsent === true || dirEmpty === true;
884
+ if (discoveryBroken === true) {
885
+ // A per-doc read failure (EACCES on one plan) lives in results[].reason; only a
886
+ // missing/unreadable DIRECTORY carries a top-level reason (review M115 run 7).
887
+ const brokenWhy = tpResults.length > 0
888
+ ? formatMalformedPlanSummary(tpResults.filter((r) => r.ok === false), [], tpEnv.reason)
889
+ : String(tpEnv.reason);
890
+ log(`M115 test-plan gate: discovery failed (${brokenWhy}) — halting (testplan-discovery-error), never a skip`);
891
+ testPlanGateResult = {
892
+ status: "testplan-gate-failed",
893
+ overallVerdict: "VERIFY-FAILED",
894
+ reason: `testplan-discovery-error: ${brokenWhy} — a gate that cannot read its plans must HALT, never skip`,
895
+ testPlanGate: { discovery: tpEnv, reason: "testplan-discovery-error" },
896
+ guardMap: { discovery: guardMapDiscovery, results: guardMapResults },
897
+ verifyGate: vg.envelope,
898
+ autoResearchGate: arGate,
899
+ };
900
+ } else if (noPlanToGate === true) {
901
+ // Nothing to gate (no .gsd-t/test-plans dir, or it holds no TestPlan-*.md) — a
902
+ // project with no test plan has legitimately nothing to check. SURFACED with
903
+ // the named reason, never silent.
904
+ log(`M115 test-plan gate: SKIP — no-test-plan (no .gsd-t/test-plans/TestPlan-*.md found)`);
905
+ } else if (testPlanDiscovery.ok === false) {
906
+ const malformed = tpResults.filter((r) => r.ok === false);
907
+ const allViolations = Array.isArray(tpEnv.violations) ? tpEnv.violations : [];
908
+ const named = formatMalformedPlanSummary(malformed, allViolations, tpEnv.reason);
909
+ log(`M115 test-plan gate FAIL exitCode=${testPlanDiscovery.exitCode} — ${malformed.length} malformed plan(s): ${named} — halting before triad`);
910
+ testPlanGateResult = {
911
+ status: "testplan-gate-failed",
912
+ overallVerdict: "VERIFY-FAILED",
913
+ reason: `M115 test-plan shape violation(s): ${named}. See templates/TestPlan-spec.md and .gsd-t/contracts/test-plan-first-contract.md §2-4.`,
914
+ testPlanGate: { discovery: tpEnv },
915
+ guardMap: { discovery: guardMapDiscovery, results: guardMapResults },
916
+ verifyGate: vg.envelope,
917
+ autoResearchGate: arGate,
918
+ };
919
+ } else {
920
+ log(`M115 test-plan gate: PASS (${tpEnv.docsChecked || 0} plan(s) checked, all clean) — proceeding to orthogonal triad`);
921
+ }
922
+ }
923
+ if (testPlanGateResult !== null) {
924
+ return testPlanGateResult;
925
+ }
926
+
801
927
  phase("Orthogonal Triad");
802
928
 
803
929
  const briefRef = brief.briefPath || "(brief generation failed — re-walk repo)";