@gobing-ai/spur 0.4.4 → 0.4.5

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/spur.js CHANGED
@@ -88794,13 +88794,12 @@ function aggregateVerifyVerdict(input) {
88794
88794
  return "PARTIAL";
88795
88795
  return "PASS";
88796
88796
  }
88797
- var VERDICT_AGGREGATES, ROW_STATUSES, CHECK_SEVERITIES, CONFIDENCE_LEVELS, verdictSchema, coverageRowSchema, checkSchema, verifyVerdictSchema, NORM = (s) => String(s ?? "").toUpperCase();
88797
+ var VERDICT_AGGREGATES, ROW_STATUSES, CHECK_SEVERITIES, verdictSchema, coverageRowSchema, checkSchema, verifyVerdictSchema, NORM = (s) => String(s ?? "").toUpperCase();
88798
88798
  var init_verify_verdict = __esm(() => {
88799
88799
  init_zod();
88800
88800
  VERDICT_AGGREGATES = ["PASS", "PARTIAL", "FAIL", "UNKNOWN"];
88801
88801
  ROW_STATUSES = ["MET", "PARTIAL", "UNMET", "N/A"];
88802
88802
  CHECK_SEVERITIES = ["blocker", "major", "minor", "advisory"];
88803
- CONFIDENCE_LEVELS = ["HIGH", "MEDIUM", "LOW"];
88804
88803
  verdictSchema = string2().transform((v, ctx) => {
88805
88804
  const up = v.toUpperCase();
88806
88805
  if (!VERDICT_AGGREGATES.includes(up)) {
@@ -88856,18 +88855,6 @@ var init_verify_verdict = __esm(() => {
88856
88855
  acceptanceCriteria: array(coverageRowSchema).optional().default([]),
88857
88856
  checks: array(checkSchema).optional().default([]),
88858
88857
  source: string2().optional(),
88859
- confidence: string2().transform((c, ctx) => {
88860
- const up = c.toUpperCase();
88861
- if (!CONFIDENCE_LEVELS.includes(up)) {
88862
- ctx.addIssue({
88863
- code: "custom",
88864
- message: `invalid confidence "${c}" (expected HIGH|MEDIUM|LOW)`,
88865
- path: ["confidence"]
88866
- });
88867
- return "LOW";
88868
- }
88869
- return up;
88870
- }).optional(),
88871
88858
  pipelineRunId: string2().optional(),
88872
88859
  recordedAt: string2().optional()
88873
88860
  });
@@ -90130,19 +90117,6 @@ async function readVerdictFrom(fs, path, wbs) {
90130
90117
  };
90131
90118
  }
90132
90119
  }
90133
- if ("confidence" in parsed) {
90134
- const rawConfidence = parsed.confidence;
90135
- const up = typeof rawConfidence === "string" ? rawConfidence.toUpperCase() : "";
90136
- if (up !== "HIGH" && up !== "MEDIUM" && up !== "LOW") {
90137
- return {
90138
- artifact: undefined,
90139
- path,
90140
- readError: `invalid confidence field at ${path}: expected HIGH|MEDIUM|LOW, actual ${JSON.stringify(rawConfidence)}`,
90141
- missing: false
90142
- };
90143
- }
90144
- parsed.confidence = up;
90145
- }
90146
90120
  return { artifact: parsed, path, missing: false };
90147
90121
  }
90148
90122
  async function readVerdictArtifact(fs, runDir, wbs) {
@@ -90237,19 +90211,6 @@ function evaluateDoneTransition(input) {
90237
90211
  const acs = artifact.acceptanceCriteria ?? [];
90238
90212
  const internallyConsistentPass = artifact.verdict !== "PASS" || reqs.length > 0 || acs.length > 0;
90239
90213
  const effective = internallyConsistentPass ? harshnessMax(artifact.verdict, computed) : "UNKNOWN";
90240
- if (effective === "PASS" && artifact.confidence === undefined) {
90241
- return {
90242
- kind: "deny",
90243
- verdict: "PASS",
90244
- message: [
90245
- `Cannot transition task ${wbs} to done: PASS verdict does not state a confidence level.`,
90246
- ` task: ${taskFilePath}`,
90247
- ` verdict: ${verdictPath} (missing \`confidence\`)`,
90248
- ` remediation: add "confidence": "HIGH|MEDIUM|LOW" to the artifact ` + `(re-run \`/sp:dev-verify ${wbs}\`), ` + `or override with \`spur task update ${wbs} done --force-done --reason "<why>"\`.`
90249
- ].join(`
90250
- `)
90251
- };
90252
- }
90253
90214
  if (effective === "PASS") {
90254
90215
  return { kind: "allow", reason: "pass" };
90255
90216
  }
@@ -91196,8 +91157,6 @@ function renderTesting(v) {
91196
91157
  lines.push("**Pipeline verify results**");
91197
91158
  lines.push("");
91198
91159
  lines.push(`- Verdict: ${v.verdict} (from verdict artifact)`);
91199
- if (v.confidence !== undefined)
91200
- lines.push(`- Confidence: ${v.confidence}`);
91201
91160
  lines.push("");
91202
91161
  if (v.requirements.length === 0) {
91203
91162
  lines.push("| Requirement | Status | Evidence |");
@@ -91629,11 +91588,6 @@ function lintVerifyAnswer(answerText, taskContent, featureContent) {
91629
91588
  } else if (!/^(PASS|PARTIAL|FAIL)$/i.test(tables.verdict.value)) {
91630
91589
  add(tables.verdict.line, "verdict-value", `invalid Verdict value "${tables.verdict.value}" (PASS | PARTIAL | FAIL)`);
91631
91590
  }
91632
- if (tables.confidence === null) {
91633
- add(0, "confidence-missing", "no `Confidence:` line (expected exactly one `Confidence: HIGH|MEDIUM|LOW` line)");
91634
- } else if (!/^(HIGH|MEDIUM|LOW)$/i.test(tables.confidence.value)) {
91635
- add(tables.confidence.line, "confidence-value", `invalid Confidence value "${tables.confidence.value}" (HIGH | MEDIUM | LOW)`);
91636
- }
91637
91591
  const reqIds = extractRequirementIds(taskContent);
91638
91592
  const acIndex = buildAcIdentityIndex(taskContent, featureContent ?? null);
91639
91593
  const seenReq = new Set;
@@ -91693,19 +91647,6 @@ function lintVerifyAnswer(answerText, taskContent, featureContent) {
91693
91647
  add(row.line, "evidence-hedged", hedgedMessage(`AC "${row.id.slice(0, 40)}"`, hedge));
91694
91648
  }
91695
91649
  }
91696
- const confidenceLevel = tables.confidence === null ? null : tables.confidence.value.toUpperCase();
91697
- if (confidenceLevel === "HIGH" || confidenceLevel === "LOW") {
91698
- const reqStatuses = tables.reqs.map((row) => normalizeReqStatus(row.status));
91699
- const acStatuses = tables.acs.map((row) => normalizeAcStatus(row.status));
91700
- const allRowsMet = reqStatuses.length > 0 && reqStatuses.every((status) => status === "MET") && acStatuses.every((status) => status === "MET");
91701
- const hedgedRow = [...tables.reqs, ...tables.acs].some((row) => (normalizeReqStatus(row.status) ?? normalizeAcStatus(row.status)) === "MET" && firstHedgedPhrase(row.evidence) !== null);
91702
- const line = tables.confidence?.line ?? 0;
91703
- if (confidenceLevel === "HIGH" && (!allRowsMet || hedgedRow)) {
91704
- add(line, "confidence-unwarranted", "Confidence HIGH is not earned by the rows: at least one row is not MET, or a MET " + "row hedges its evidence (downgrade the level, or finish the row)");
91705
- } else if (confidenceLevel === "LOW" && allRowsMet) {
91706
- add(line, "confidence-understated", "Confidence LOW understates an answer whose every row is MET (raise the level to " + "HIGH or MEDIUM)");
91707
- }
91708
- }
91709
91650
  return findings;
91710
91651
  }
91711
91652
  function hasClaimMarker(evidence) {
@@ -91780,7 +91721,7 @@ function normalizeEvidenceType(raw) {
91780
91721
  return EVIDENCE_TYPE_PRECEDENCE.find((candidate) => tokens.includes(candidate)) ?? null;
91781
91722
  }
91782
91723
  function parseAnswer(text) {
91783
- const out = { verdict: null, confidence: null, reqs: [], acs: [] };
91724
+ const out = { verdict: null, reqs: [], acs: [] };
91784
91725
  const lines = text.split(`
91785
91726
  `);
91786
91727
  let reqTable = false;
@@ -91840,26 +91781,11 @@ function parseAnswer(text) {
91840
91781
  out.verdict = {
91841
91782
  value: verdictMatches[0]?.[1] ?? "",
91842
91783
  line: text.slice(0, verdictMatches[0]?.index ?? 0).split(`
91843
- `).length
91844
- };
91845
- }
91846
- const confidenceMatches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
91847
- if (confidenceMatches.length === 1) {
91848
- out.confidence = {
91849
- value: confidenceMatches[0]?.[1] ?? "",
91850
- line: text.slice(0, confidenceMatches[0]?.index ?? 0).split(`
91851
91784
  `).length
91852
91785
  };
91853
91786
  }
91854
91787
  return out;
91855
91788
  }
91856
- function extractAnswerConfidence(text) {
91857
- const matches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
91858
- if (matches.length !== 1)
91859
- return;
91860
- const value = (matches[0]?.[1] ?? "").toUpperCase();
91861
- return CONFIDENCE_LEVELS2.includes(value) ? value : undefined;
91862
- }
91863
91789
  function sectionBetween(text, heading) {
91864
91790
  const marker = new RegExp(`^#{1,6}\\s+${heading}\\s*$`, "im");
91865
91791
  const match = marker.exec(text);
@@ -91969,7 +91895,7 @@ function resolveAcIdentity(rowId, index) {
91969
91895
  }
91970
91896
  return { ok: false, error: "" };
91971
91897
  }
91972
- var ANSWER_LINT_MAX_FINDINGS = 10, CLAIM_MARKERS, CITATION_FORMS, HEDGED_PATTERNS, EVIDENCE_TYPE_PRECEDENCE, CONFIDENCE_LEVELS2;
91898
+ var ANSWER_LINT_MAX_FINDINGS = 10, CLAIM_MARKERS, CITATION_FORMS, HEDGED_PATTERNS, EVIDENCE_TYPE_PRECEDENCE;
91973
91899
  var init_verify_answer_lint = __esm(() => {
91974
91900
  CLAIM_MARKERS = [
91975
91901
  /@[a-z0-9._-]+\/[a-z0-9._-]+/i,
@@ -91993,7 +91919,6 @@ var init_verify_answer_lint = __esm(() => {
91993
91919
  /\bit could\b/i
91994
91920
  ];
91995
91921
  EVIDENCE_TYPE_PRECEDENCE = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
91996
- CONFIDENCE_LEVELS2 = ["HIGH", "MEDIUM", "LOW"];
91997
91922
  });
91998
91923
 
91999
91924
  // ../../packages/app/src/services/feature-check.ts
@@ -114609,329 +114534,6 @@ var init_task_transition = __esm(() => {
114609
114534
  init_done_transition_guard();
114610
114535
  });
114611
114536
 
114612
- // ../../packages/app/src/services/task-verdict.ts
114613
- function confidenceCheckRow(confidence, requirements, acceptanceCriteria) {
114614
- const rows = [
114615
- ...requirements,
114616
- ...acceptanceCriteria ?? []
114617
- ];
114618
- const notMet = rows.filter((r) => r.status !== "MET" && r.status !== "N/A");
114619
- const hedged = rows.filter((r) => r.status === "MET").map((r) => firstHedgedPhrase(r.evidence ?? "")).find((phrase) => phrase !== null);
114620
- const level = confidence ?? "LOW";
114621
- const earned = confidence !== undefined && level !== "LOW" && notMet.length === 0 && hedged === undefined;
114622
- const basis = `${confidence === undefined ? "LOW (absent from the answer)" : level} \u2014 ${rows.length - notMet.length}/${rows.length} rows MET` + (notMet.length > 0 ? ` (not MET: ${notMet.map((r) => r.id).join(", ")})` : "") + (hedged !== undefined ? `; hedged MET evidence: "${hedged}"` : "");
114623
- return { name: "confidence", status: earned ? "pass" : "warn", evidence: basis };
114624
- }
114625
- function injectEarnedConfidenceRow(text) {
114626
- let parsed;
114627
- try {
114628
- parsed = JSON.parse(text);
114629
- } catch {
114630
- return text;
114631
- }
114632
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
114633
- return text;
114634
- const artifact = parsed;
114635
- const checks = Array.isArray(artifact.checks) ? artifact.checks : [];
114636
- if (checks.some((c) => c !== null && typeof c === "object" && c.name === "confidence"))
114637
- return text;
114638
- const level = typeof artifact.confidence === "string" ? artifact.confidence : undefined;
114639
- const row = confidenceCheckRow(level, artifact.requirements ?? [], artifact.acceptanceCriteria);
114640
- if (row.status !== "pass")
114641
- return text;
114642
- return `${JSON.stringify({ ...artifact, checks: [...checks, row] }, null, 4)}
114643
- `;
114644
- }
114645
- function deriveVerdict(answerText, taskCheckPassed) {
114646
- const requirements = extractRequirements(answerText);
114647
- const parsedAc = extractAcceptanceCriteria(answerText);
114648
- const acceptanceCriteria = applyAcceptanceCriteriaEvidenceRule(parsedAc.rows);
114649
- const checks = extractChecks(answerText, taskCheckPassed, requirements, acceptanceCriteria, parsedAc.dropped);
114650
- const confidence = extractAnswerConfidence(answerText);
114651
- const checksWithConfidence = [
114652
- ...checks.filter((c) => c.name !== "confidence"),
114653
- confidenceCheckRow(confidence, requirements, acceptanceCriteria)
114654
- ];
114655
- if (requirements.length === 0) {
114656
- return {
114657
- verdict: "UNKNOWN",
114658
- ...confidence !== undefined ? { confidence } : {},
114659
- requirements,
114660
- acceptanceCriteria,
114661
- checks: checksWithConfidence
114662
- };
114663
- }
114664
- const aggregate = aggregateVerifyVerdict({
114665
- requirements,
114666
- acceptanceCriteria,
114667
- checks: [],
114668
- taskCheckPassed
114669
- });
114670
- return {
114671
- verdict: aggregate,
114672
- ...confidence !== undefined ? { confidence } : {},
114673
- requirements,
114674
- acceptanceCriteria,
114675
- checks: checksWithConfidence
114676
- };
114677
- }
114678
- function extractRequirements(text) {
114679
- const reqs = [];
114680
- const lines = text.split(`
114681
- `);
114682
- let inTable = false;
114683
- let colMap = null;
114684
- for (const line of lines) {
114685
- const trimmed = line.trim();
114686
- if (inTable && /^#{1,6}\s/.test(trimmed)) {
114687
- inTable = false;
114688
- colMap = null;
114689
- continue;
114690
- }
114691
- if (!trimmed.startsWith("|"))
114692
- continue;
114693
- const cells = splitTableCells2(trimmed);
114694
- if (!inTable && cells.length >= 2) {
114695
- const h0 = (cells[0] ?? "").toLowerCase().trim();
114696
- const h0IsId = h0.includes("req") || h0 === "requirement" || h0 === "r#" || h0 === "r" || /^r\d+$/.test(h0);
114697
- if (h0IsId) {
114698
- const statusIdx = cells.findIndex((c) => {
114699
- const x = c.toLowerCase().trim();
114700
- return x.includes("status") || x === "verdict";
114701
- });
114702
- if (statusIdx >= 0) {
114703
- const evidenceIdx = cells.findIndex((c) => c.toLowerCase().includes("evidence"));
114704
- colMap = {
114705
- id: 0,
114706
- status: statusIdx,
114707
- evidence: evidenceIdx >= 0 ? evidenceIdx : undefined
114708
- };
114709
- inTable = true;
114710
- continue;
114711
- }
114712
- }
114713
- }
114714
- if (/^[-:]+$/.test(cells[0] ?? ""))
114715
- continue;
114716
- if (inTable && colMap && cells.length >= 2) {
114717
- const first = (cells[0] ?? "").toLowerCase();
114718
- const second = (cells[1] ?? "").toLowerCase();
114719
- if ((first === "ac" || first.includes("acceptance") || first === "check" || first === "name") && (second.includes("status") || second === "verdict")) {
114720
- inTable = false;
114721
- colMap = null;
114722
- continue;
114723
- }
114724
- const id = cells[colMap.id] ?? "";
114725
- const statusRaw = (cells[colMap.status] ?? "").toUpperCase();
114726
- const evidence = colMap.evidence !== undefined ? cells[colMap.evidence] ?? "" : "";
114727
- const status = normalizeStatus(statusRaw);
114728
- if (status !== null && id.length > 0) {
114729
- reqs.push({ id, status, evidence });
114730
- }
114731
- }
114732
- }
114733
- return reqs;
114734
- }
114735
- function normalizeStatus(raw) {
114736
- if (/\bMET\b/.test(raw))
114737
- return "MET";
114738
- if (/\bPARTIAL\b/.test(raw))
114739
- return "PARTIAL";
114740
- if (/\bUNMET\b/.test(raw))
114741
- return "UNMET";
114742
- return null;
114743
- }
114744
- function splitTableCells2(row) {
114745
- return row.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, "|").trim()).filter(Boolean);
114746
- }
114747
- function extractAcceptanceCriteria(text) {
114748
- const rows = [];
114749
- const dropped = [];
114750
- const lines = text.split(`
114751
- `);
114752
- let inTable = false;
114753
- for (const line of lines) {
114754
- const trimmed = line.trim();
114755
- if (inTable && /^#{1,6}\s/.test(trimmed)) {
114756
- inTable = false;
114757
- continue;
114758
- }
114759
- if (!trimmed.startsWith("|"))
114760
- continue;
114761
- const cells = splitTableCells2(trimmed);
114762
- if (!inTable && cells.length >= 4) {
114763
- const h0 = (cells[0] ?? "").toLowerCase();
114764
- const h1 = (cells[1] ?? "").toLowerCase();
114765
- const h2 = (cells[2] ?? "").toLowerCase();
114766
- if ((h0 === "ac" || h0.includes("acceptance")) && h1.includes("status") && h2.includes("evidence")) {
114767
- inTable = true;
114768
- continue;
114769
- }
114770
- }
114771
- if (/^[-:]+$/.test(cells[0] ?? ""))
114772
- continue;
114773
- if (inTable && cells.length >= 3) {
114774
- const id = cells[0] ?? "";
114775
- const statusRaw = cells[1] ?? "";
114776
- const evidenceTypeRaw = cells[2] ?? "";
114777
- const status = normalizeAcceptanceCriteriaStatus(statusRaw);
114778
- const evidenceType = normalizeEvidenceType2(evidenceTypeRaw);
114779
- const acShaped = cells.length >= 4 || id.length > 0 && status !== null;
114780
- const evidence = cells[3] ?? "";
114781
- if (acShaped && status !== null && evidenceType !== null && id.length > 0) {
114782
- rows.push({ id, status, evidenceType, evidence });
114783
- } else if (acShaped && id.length > 0) {
114784
- dropped.push(evidenceType === null && status !== null ? `${id} (unrecognised evidence type "${evidenceTypeRaw}")` : `${id} (unrecognised status "${statusRaw}")`);
114785
- }
114786
- }
114787
- }
114788
- return { rows, dropped };
114789
- }
114790
- function normalizeAcceptanceCriteriaStatus(raw) {
114791
- const upper = raw.toUpperCase();
114792
- if (/\bMET\b/.test(upper))
114793
- return "MET";
114794
- if (/\bPARTIAL\b/.test(upper))
114795
- return "PARTIAL";
114796
- if (/\bUNMET\b/.test(upper))
114797
- return "UNMET";
114798
- if (/\bN\/A\b/.test(upper) || /\bNA\b/.test(upper))
114799
- return "N/A";
114800
- return null;
114801
- }
114802
- function normalizeEvidenceTypeToken2(normalized) {
114803
- if (normalized === "test")
114804
- return "test";
114805
- if (normalized === "command")
114806
- return "command";
114807
- if (normalized === "static-ref" || normalized === "static" || normalized === "doc" || normalized === "docs" || normalized === "documentation") {
114808
- return "static-ref";
114809
- }
114810
- if (normalized === "manual-review" || normalized === "manual")
114811
- return "manual-review";
114812
- if (normalized === "llm-judge" || normalized === "judge")
114813
- return "llm-judge";
114814
- if (normalized === "n/a" || normalized === "na")
114815
- return "n/a";
114816
- return null;
114817
- }
114818
- function normalizeEvidenceType2(raw) {
114819
- const normalized = raw.toLowerCase().trim();
114820
- const single = normalizeEvidenceTypeToken2(normalized);
114821
- if (single !== null)
114822
- return single;
114823
- const tokens = normalized.split(/[+,/]/).map((part) => normalizeEvidenceTypeToken2(part.trim())).filter((token) => token !== null);
114824
- if (tokens.length < 2 || tokens.length !== normalized.split(/[+,/]/).filter((p) => p.trim()).length) {
114825
- return null;
114826
- }
114827
- return EVIDENCE_TYPE_PRECEDENCE2.find((candidate) => tokens.includes(candidate)) ?? null;
114828
- }
114829
- function applyAcceptanceCriteriaEvidenceRule(rows) {
114830
- return rows.map((row) => {
114831
- if (row.status === "MET" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command") {
114832
- return { ...row, status: "PARTIAL" };
114833
- }
114834
- return row;
114835
- });
114836
- }
114837
- function requiresExecutableEvidence(id) {
114838
- const normalized = id.toLowerCase();
114839
- const nonCore = normalized.includes("[advisory]") || normalized.includes("[non-core]");
114840
- const nonBehavior = normalized.includes("[non-behavior]") || normalized.includes("[docs-only]") || normalized.includes("[doc-only]");
114841
- return !nonCore && !nonBehavior;
114842
- }
114843
- function extractChecks(_text, taskCheckPassed, requirements, acceptanceCriteria, droppedAcRows = []) {
114844
- const checks = [
114845
- {
114846
- name: "spur task check",
114847
- status: taskCheckPassed ? "pass" : "fail",
114848
- evidence: taskCheckPassed ? "task check passed" : "task check failed"
114849
- }
114850
- ];
114851
- const lines = _text.split(`
114852
- `);
114853
- let inChecks = false;
114854
- for (const line of lines) {
114855
- const trimmed = line.trim();
114856
- if (/^#{1,6}\s/.test(trimmed))
114857
- inChecks = false;
114858
- if (!trimmed.startsWith("|"))
114859
- continue;
114860
- const cells = splitTableCells2(trimmed);
114861
- if (cells.length >= 2) {
114862
- const h0 = (cells[0] ?? "").toLowerCase();
114863
- const h1 = (cells[1] ?? "").toLowerCase();
114864
- if ((h0 === "check" || h0 === "name") && h1 === "status") {
114865
- inChecks = true;
114866
- continue;
114867
- }
114868
- if (inChecks && cells.length >= 3 && !/^[-:]+$/.test(cells[0] ?? "")) {
114869
- checks.push({
114870
- name: cells[0] ?? "",
114871
- status: (cells[1] ?? "").toLowerCase(),
114872
- evidence: cells[2] ?? ""
114873
- });
114874
- }
114875
- }
114876
- }
114877
- if (droppedAcRows.length > 0) {
114878
- checks.push({
114879
- name: "ac-row-dropped",
114880
- status: "fail",
114881
- evidence: `${droppedAcRows.length} AC row(s) could not be parsed and were omitted from the verdict: ` + `${droppedAcRows.join("; ")}. Accepted evidence types: test, command, static-ref (aliases: ` + "static, doc, docs, documentation), manual-review, llm-judge, n/a. Accepted statuses: " + "MET, PARTIAL, UNMET, N/A."
114882
- });
114883
- }
114884
- const hollowMet = [
114885
- ...requirements.filter((r) => r.status === "MET" && r.evidence.trim() === "").map((r) => r.id),
114886
- ...acceptanceCriteria.filter((r) => r.status === "MET" && r.evidence.trim() === "").map((r) => r.id)
114887
- ];
114888
- if (hollowMet.length > 0) {
114889
- checks.push({
114890
- name: "hollow-met-evidence",
114891
- status: "fail",
114892
- severity: "major",
114893
- evidence: `${hollowMet.length} MET row(s) carry no recorded evidence and cannot support PASS: ` + `${hollowMet.join(", ")}. Re-run verify with populated evidence per row.`
114894
- });
114895
- }
114896
- if (acceptanceCriteria.length > 0) {
114897
- const downgraded = acceptanceCriteria.filter((row) => row.status === "PARTIAL" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command");
114898
- checks.push({
114899
- name: downgraded.length > 0 ? "evidence-rule-failed" : "evidence-rule-pass",
114900
- status: downgraded.length > 0 ? "fail" : "pass",
114901
- evidence: downgraded.length > 0 ? `Executable evidence missing for: ${downgraded.map((row) => row.id).join(", ")}` : "All behavior-bearing AC rows have executable evidence or are explicitly non-behavioral."
114902
- });
114903
- }
114904
- return checks;
114905
- }
114906
- function aggregateBatchVerdicts(results) {
114907
- const notStarted = results.filter((r) => r.outcome === "NOT-STARTED");
114908
- const rolledUp = results.filter((r) => r.outcome !== "NOT-STARTED");
114909
- const counts = {
114910
- PASS: rolledUp.filter((r) => r.outcome === "PASS").length,
114911
- PARTIAL: rolledUp.filter((r) => r.outcome === "PARTIAL").length,
114912
- FAIL: rolledUp.filter((r) => r.outcome === "FAIL").length,
114913
- UNKNOWN: rolledUp.filter((r) => r.outcome === "UNKNOWN").length
114914
- };
114915
- let verdict;
114916
- if (rolledUp.length === 0) {
114917
- verdict = "UNKNOWN";
114918
- } else if (counts.FAIL > 0) {
114919
- verdict = "FAIL";
114920
- } else if (counts.PARTIAL > 0 || counts.UNKNOWN > 0) {
114921
- verdict = "PARTIAL";
114922
- } else {
114923
- verdict = "PASS";
114924
- }
114925
- const summary = `${counts.PASS} PASS, ${counts.PARTIAL} PARTIAL, ${counts.FAIL} FAIL, ${notStarted.length} NOT-STARTED (excluded)`;
114926
- return { verdict, rolledUp, notStarted, summary };
114927
- }
114928
- var EVIDENCE_TYPE_PRECEDENCE2;
114929
- var init_task_verdict = __esm(() => {
114930
- init_verify_answer_lint();
114931
- init_verify_verdict();
114932
- EVIDENCE_TYPE_PRECEDENCE2 = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
114933
- });
114934
-
114935
114537
  // ../../packages/app/src/services/task-service.ts
114936
114538
  import { dirname as dirname32, isAbsolute as isAbsolute12, join as join57, relative as relative13, sep as sep13 } from "path";
114937
114539
  function citationOrigin(verdict, name) {
@@ -115680,7 +115282,7 @@ ${templateBackground}`;
115680
115282
  const bytes = await this.ctx.fs.readFile(verdictPath);
115681
115283
  ensureDurablePlaneIgnored(paths.projectRoot);
115682
115284
  await this.ctx.fs.ensureDir(dirname32(durablePath));
115683
- await atomicWriteAsync(durablePath, injectEarnedConfidenceRow(bytes), wbs, this.ctx.fs, this.ctx.projectName ?? "spur");
115285
+ await atomicWriteAsync(durablePath, bytes, wbs, this.ctx.fs, this.ctx.projectName ?? "spur");
115684
115286
  }
115685
115287
  const result = {
115686
115288
  testingWritten: false,
@@ -116235,7 +115837,6 @@ var init_task_service = __esm(() => {
116235
115837
  init_task_section_normalizer();
116236
115838
  init_task_size_precheck();
116237
115839
  init_task_transition();
116238
- init_task_verdict();
116239
115840
  DependencyMutationError = class DependencyMutationError extends Error {
116240
115841
  code;
116241
115842
  constructor(code, message) {
@@ -116315,6 +115916,289 @@ var init_task_service = __esm(() => {
116315
115916
  };
116316
115917
  });
116317
115918
 
115919
+ // ../../packages/app/src/services/task-verdict.ts
115920
+ function deriveVerdict(answerText, taskCheckPassed) {
115921
+ const requirements = extractRequirements(answerText);
115922
+ const parsedAc = extractAcceptanceCriteria(answerText);
115923
+ const acceptanceCriteria = applyAcceptanceCriteriaEvidenceRule(parsedAc.rows);
115924
+ const checks = extractChecks(answerText, taskCheckPassed, requirements, acceptanceCriteria, parsedAc.dropped);
115925
+ if (requirements.length === 0) {
115926
+ return {
115927
+ verdict: "UNKNOWN",
115928
+ requirements,
115929
+ acceptanceCriteria,
115930
+ checks
115931
+ };
115932
+ }
115933
+ const aggregate = aggregateVerifyVerdict({
115934
+ requirements,
115935
+ acceptanceCriteria,
115936
+ checks: [],
115937
+ taskCheckPassed
115938
+ });
115939
+ return {
115940
+ verdict: aggregate,
115941
+ requirements,
115942
+ acceptanceCriteria,
115943
+ checks
115944
+ };
115945
+ }
115946
+ function extractRequirements(text) {
115947
+ const reqs = [];
115948
+ const lines = text.split(`
115949
+ `);
115950
+ let inTable = false;
115951
+ let colMap = null;
115952
+ for (const line of lines) {
115953
+ const trimmed = line.trim();
115954
+ if (inTable && /^#{1,6}\s/.test(trimmed)) {
115955
+ inTable = false;
115956
+ colMap = null;
115957
+ continue;
115958
+ }
115959
+ if (!trimmed.startsWith("|"))
115960
+ continue;
115961
+ const cells = splitTableCells2(trimmed);
115962
+ if (!inTable && cells.length >= 2) {
115963
+ const h0 = (cells[0] ?? "").toLowerCase().trim();
115964
+ const h0IsId = h0.includes("req") || h0 === "requirement" || h0 === "r#" || h0 === "r" || /^r\d+$/.test(h0);
115965
+ if (h0IsId) {
115966
+ const statusIdx = cells.findIndex((c) => {
115967
+ const x = c.toLowerCase().trim();
115968
+ return x.includes("status") || x === "verdict";
115969
+ });
115970
+ if (statusIdx >= 0) {
115971
+ const evidenceIdx = cells.findIndex((c) => c.toLowerCase().includes("evidence"));
115972
+ colMap = {
115973
+ id: 0,
115974
+ status: statusIdx,
115975
+ evidence: evidenceIdx >= 0 ? evidenceIdx : undefined
115976
+ };
115977
+ inTable = true;
115978
+ continue;
115979
+ }
115980
+ }
115981
+ }
115982
+ if (/^[-:]+$/.test(cells[0] ?? ""))
115983
+ continue;
115984
+ if (inTable && colMap && cells.length >= 2) {
115985
+ const first = (cells[0] ?? "").toLowerCase();
115986
+ const second = (cells[1] ?? "").toLowerCase();
115987
+ if ((first === "ac" || first.includes("acceptance") || first === "check" || first === "name") && (second.includes("status") || second === "verdict")) {
115988
+ inTable = false;
115989
+ colMap = null;
115990
+ continue;
115991
+ }
115992
+ const id = cells[colMap.id] ?? "";
115993
+ const statusRaw = (cells[colMap.status] ?? "").toUpperCase();
115994
+ const evidence = colMap.evidence !== undefined ? cells[colMap.evidence] ?? "" : "";
115995
+ const status = normalizeStatus(statusRaw);
115996
+ if (status !== null && id.length > 0) {
115997
+ reqs.push({ id, status, evidence });
115998
+ }
115999
+ }
116000
+ }
116001
+ return reqs;
116002
+ }
116003
+ function normalizeStatus(raw) {
116004
+ if (/\bMET\b/.test(raw))
116005
+ return "MET";
116006
+ if (/\bPARTIAL\b/.test(raw))
116007
+ return "PARTIAL";
116008
+ if (/\bUNMET\b/.test(raw))
116009
+ return "UNMET";
116010
+ return null;
116011
+ }
116012
+ function splitTableCells2(row) {
116013
+ return row.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, "|").trim()).filter(Boolean);
116014
+ }
116015
+ function extractAcceptanceCriteria(text) {
116016
+ const rows = [];
116017
+ const dropped = [];
116018
+ const lines = text.split(`
116019
+ `);
116020
+ let inTable = false;
116021
+ for (const line of lines) {
116022
+ const trimmed = line.trim();
116023
+ if (inTable && /^#{1,6}\s/.test(trimmed)) {
116024
+ inTable = false;
116025
+ continue;
116026
+ }
116027
+ if (!trimmed.startsWith("|"))
116028
+ continue;
116029
+ const cells = splitTableCells2(trimmed);
116030
+ if (!inTable && cells.length >= 4) {
116031
+ const h0 = (cells[0] ?? "").toLowerCase();
116032
+ const h1 = (cells[1] ?? "").toLowerCase();
116033
+ const h2 = (cells[2] ?? "").toLowerCase();
116034
+ if ((h0 === "ac" || h0.includes("acceptance")) && h1.includes("status") && h2.includes("evidence")) {
116035
+ inTable = true;
116036
+ continue;
116037
+ }
116038
+ }
116039
+ if (/^[-:]+$/.test(cells[0] ?? ""))
116040
+ continue;
116041
+ if (inTable && cells.length >= 3) {
116042
+ const id = cells[0] ?? "";
116043
+ const statusRaw = cells[1] ?? "";
116044
+ const evidenceTypeRaw = cells[2] ?? "";
116045
+ const status = normalizeAcceptanceCriteriaStatus(statusRaw);
116046
+ const evidenceType = normalizeEvidenceType2(evidenceTypeRaw);
116047
+ const acShaped = cells.length >= 4 || id.length > 0 && status !== null;
116048
+ const evidence = cells[3] ?? "";
116049
+ if (acShaped && status !== null && evidenceType !== null && id.length > 0) {
116050
+ rows.push({ id, status, evidenceType, evidence });
116051
+ } else if (acShaped && id.length > 0) {
116052
+ dropped.push(evidenceType === null && status !== null ? `${id} (unrecognised evidence type "${evidenceTypeRaw}")` : `${id} (unrecognised status "${statusRaw}")`);
116053
+ }
116054
+ }
116055
+ }
116056
+ return { rows, dropped };
116057
+ }
116058
+ function normalizeAcceptanceCriteriaStatus(raw) {
116059
+ const upper = raw.toUpperCase();
116060
+ if (/\bMET\b/.test(upper))
116061
+ return "MET";
116062
+ if (/\bPARTIAL\b/.test(upper))
116063
+ return "PARTIAL";
116064
+ if (/\bUNMET\b/.test(upper))
116065
+ return "UNMET";
116066
+ if (/\bN\/A\b/.test(upper) || /\bNA\b/.test(upper))
116067
+ return "N/A";
116068
+ return null;
116069
+ }
116070
+ function normalizeEvidenceTypeToken2(normalized) {
116071
+ if (normalized === "test")
116072
+ return "test";
116073
+ if (normalized === "command")
116074
+ return "command";
116075
+ if (normalized === "static-ref" || normalized === "static" || normalized === "doc" || normalized === "docs" || normalized === "documentation") {
116076
+ return "static-ref";
116077
+ }
116078
+ if (normalized === "manual-review" || normalized === "manual")
116079
+ return "manual-review";
116080
+ if (normalized === "llm-judge" || normalized === "judge")
116081
+ return "llm-judge";
116082
+ if (normalized === "n/a" || normalized === "na")
116083
+ return "n/a";
116084
+ return null;
116085
+ }
116086
+ function normalizeEvidenceType2(raw) {
116087
+ const normalized = raw.toLowerCase().trim();
116088
+ const single = normalizeEvidenceTypeToken2(normalized);
116089
+ if (single !== null)
116090
+ return single;
116091
+ const tokens = normalized.split(/[+,/]/).map((part) => normalizeEvidenceTypeToken2(part.trim())).filter((token) => token !== null);
116092
+ if (tokens.length < 2 || tokens.length !== normalized.split(/[+,/]/).filter((p) => p.trim()).length) {
116093
+ return null;
116094
+ }
116095
+ return EVIDENCE_TYPE_PRECEDENCE2.find((candidate) => tokens.includes(candidate)) ?? null;
116096
+ }
116097
+ function applyAcceptanceCriteriaEvidenceRule(rows) {
116098
+ return rows.map((row) => {
116099
+ if (row.status === "MET" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command") {
116100
+ return { ...row, status: "PARTIAL" };
116101
+ }
116102
+ return row;
116103
+ });
116104
+ }
116105
+ function requiresExecutableEvidence(id) {
116106
+ const normalized = id.toLowerCase();
116107
+ const nonCore = normalized.includes("[advisory]") || normalized.includes("[non-core]");
116108
+ const nonBehavior = normalized.includes("[non-behavior]") || normalized.includes("[docs-only]") || normalized.includes("[doc-only]");
116109
+ return !nonCore && !nonBehavior;
116110
+ }
116111
+ function extractChecks(_text, taskCheckPassed, requirements, acceptanceCriteria, droppedAcRows = []) {
116112
+ const checks = [
116113
+ {
116114
+ name: "spur task check",
116115
+ status: taskCheckPassed ? "pass" : "fail",
116116
+ evidence: taskCheckPassed ? "task check passed" : "task check failed"
116117
+ }
116118
+ ];
116119
+ const lines = _text.split(`
116120
+ `);
116121
+ let inChecks = false;
116122
+ for (const line of lines) {
116123
+ const trimmed = line.trim();
116124
+ if (/^#{1,6}\s/.test(trimmed))
116125
+ inChecks = false;
116126
+ if (!trimmed.startsWith("|"))
116127
+ continue;
116128
+ const cells = splitTableCells2(trimmed);
116129
+ if (cells.length >= 2) {
116130
+ const h0 = (cells[0] ?? "").toLowerCase();
116131
+ const h1 = (cells[1] ?? "").toLowerCase();
116132
+ if ((h0 === "check" || h0 === "name") && h1 === "status") {
116133
+ inChecks = true;
116134
+ continue;
116135
+ }
116136
+ if (inChecks && cells.length >= 3 && !/^[-:]+$/.test(cells[0] ?? "")) {
116137
+ checks.push({
116138
+ name: cells[0] ?? "",
116139
+ status: (cells[1] ?? "").toLowerCase(),
116140
+ evidence: cells[2] ?? ""
116141
+ });
116142
+ }
116143
+ }
116144
+ }
116145
+ if (droppedAcRows.length > 0) {
116146
+ checks.push({
116147
+ name: "ac-row-dropped",
116148
+ status: "fail",
116149
+ evidence: `${droppedAcRows.length} AC row(s) could not be parsed and were omitted from the verdict: ` + `${droppedAcRows.join("; ")}. Accepted evidence types: test, command, static-ref (aliases: ` + "static, doc, docs, documentation), manual-review, llm-judge, n/a. Accepted statuses: " + "MET, PARTIAL, UNMET, N/A."
116150
+ });
116151
+ }
116152
+ const hollowMet = [
116153
+ ...requirements.filter((r) => r.status === "MET" && r.evidence.trim() === "").map((r) => r.id),
116154
+ ...acceptanceCriteria.filter((r) => r.status === "MET" && r.evidence.trim() === "").map((r) => r.id)
116155
+ ];
116156
+ if (hollowMet.length > 0) {
116157
+ checks.push({
116158
+ name: "hollow-met-evidence",
116159
+ status: "fail",
116160
+ severity: "major",
116161
+ evidence: `${hollowMet.length} MET row(s) carry no recorded evidence and cannot support PASS: ` + `${hollowMet.join(", ")}. Re-run verify with populated evidence per row.`
116162
+ });
116163
+ }
116164
+ if (acceptanceCriteria.length > 0) {
116165
+ const downgraded = acceptanceCriteria.filter((row) => row.status === "PARTIAL" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command");
116166
+ checks.push({
116167
+ name: downgraded.length > 0 ? "evidence-rule-failed" : "evidence-rule-pass",
116168
+ status: downgraded.length > 0 ? "fail" : "pass",
116169
+ evidence: downgraded.length > 0 ? `Executable evidence missing for: ${downgraded.map((row) => row.id).join(", ")}` : "All behavior-bearing AC rows have executable evidence or are explicitly non-behavioral."
116170
+ });
116171
+ }
116172
+ return checks;
116173
+ }
116174
+ function aggregateBatchVerdicts(results) {
116175
+ const notStarted = results.filter((r) => r.outcome === "NOT-STARTED");
116176
+ const rolledUp = results.filter((r) => r.outcome !== "NOT-STARTED");
116177
+ const counts = {
116178
+ PASS: rolledUp.filter((r) => r.outcome === "PASS").length,
116179
+ PARTIAL: rolledUp.filter((r) => r.outcome === "PARTIAL").length,
116180
+ FAIL: rolledUp.filter((r) => r.outcome === "FAIL").length,
116181
+ UNKNOWN: rolledUp.filter((r) => r.outcome === "UNKNOWN").length
116182
+ };
116183
+ let verdict;
116184
+ if (rolledUp.length === 0) {
116185
+ verdict = "UNKNOWN";
116186
+ } else if (counts.FAIL > 0) {
116187
+ verdict = "FAIL";
116188
+ } else if (counts.PARTIAL > 0 || counts.UNKNOWN > 0) {
116189
+ verdict = "PARTIAL";
116190
+ } else {
116191
+ verdict = "PASS";
116192
+ }
116193
+ const summary = `${counts.PASS} PASS, ${counts.PARTIAL} PARTIAL, ${counts.FAIL} FAIL, ${notStarted.length} NOT-STARTED (excluded)`;
116194
+ return { verdict, rolledUp, notStarted, summary };
116195
+ }
116196
+ var EVIDENCE_TYPE_PRECEDENCE2;
116197
+ var init_task_verdict = __esm(() => {
116198
+ init_verify_verdict();
116199
+ EVIDENCE_TYPE_PRECEDENCE2 = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
116200
+ });
116201
+
116318
116202
  // ../../packages/app/src/services/token-ledger-service.ts
116319
116203
  import { closeSync as closeSync6, existsSync as existsSync19, fstatSync as fstatSync2, openSync as openSync6, readSync } from "fs";
116320
116204
  import { join as join58 } from "path";
@@ -134429,7 +134313,7 @@ import { createRequire as createRequire3 } from "module";
134429
134313
  var CLI_CONFIG = {
134430
134314
  binaryName: "spur",
134431
134315
  binaryLabel: "spur",
134432
- binaryVersion: "0.4.4",
134316
+ binaryVersion: "0.4.5",
134433
134317
  configDir: ".spur",
134434
134318
  configFile: ".spur/config.yaml",
134435
134319
  databaseFile: ".spur/spur.db"
@@ -143057,7 +142941,7 @@ await init_src3();
143057
142941
  import { homedir as homedir14 } from "os";
143058
142942
  import { basename as basename20, join as join70, relative as relative14, resolve as resolve28 } from "path";
143059
142943
  // ../server/package.json
143060
- var version3 = "0.4.4";
142944
+ var version3 = "0.4.5";
143061
142945
 
143062
142946
  // ../server/src/modules/health/index.ts
143063
142947
  var startedAt = Date.now();