substrate-ai 0.21.4 → 0.21.6
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/dist/adapter-registry-Csta1uJX.js +4 -0
- package/dist/cli/index.js +32 -16
- package/dist/{decisions-lxU14pNh.js → decisions-Blw6BzGL.js} +1 -1
- package/dist/{dist-CpnLJuVw.js → dist-DeyV_vqU.js} +3 -2
- package/dist/{errors-8tVEwG6v.js → errors-BgV6pqUZ.js} +2 -2
- package/dist/{experimenter-C7swe92R.js → experimenter-DDuRR16z.js} +1 -1
- package/dist/{health-DklP4Se9.js → health-Chn5HCft.js} +3 -3
- package/dist/{health-BUl3y64N.js → health-HpFVnRU0.js} +3 -3
- package/dist/index.d.ts +57 -1
- package/dist/index.js +2 -2
- package/dist/{interactive-prompt-C4Is1QDB.js → interactive-prompt-j4H9HnfF.js} +2 -2
- package/dist/{manifest-read-DZDlSq_s.js → manifest-read-D7HcgX21.js} +481 -383
- package/dist/modules/interactive-prompt/index.js +3 -3
- package/dist/{routing-CGO2UZyA.js → routing-uZdhcjzV.js} +1 -1
- package/dist/{run-nSb1TD3o.js → run-BbBSwrqC.js} +205 -8
- package/dist/{run-CMXmzqoy.js → run-BrltNj0g.js} +5 -5
- package/dist/src/modules/recovery-engine/index.d.ts +9 -0
- package/dist/{upgrade-M0UkDCeg.js → upgrade-BkP-yFWV.js} +2 -2
- package/dist/{upgrade-BUSzXgnd.js → upgrade-lE_GIvWv.js} +2 -2
- package/dist/{version-manager-impl-DeEPhQlA.js → version-manager-impl-Cc7yx4i5.js} +1 -1
- package/package.json +1 -1
- package/packs/bmad/manifest.yaml +3 -0
- package/packs/bmad/prompts/acceptance-judge.md +58 -0
- package/dist/adapter-registry-BSig1zpx.js +0 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "./logger-KeHncl-f.js";
|
|
2
|
-
import { LEARNING_FINDING, createDecision, getDecisionsByCategory } from "./dist-
|
|
2
|
+
import { LEARNING_FINDING, createDecision, getDecisionsByCategory } from "./dist-DeyV_vqU.js";
|
|
3
3
|
import * as path$1 from "path";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { readFile } from "fs/promises";
|
|
@@ -2613,13 +2613,13 @@ var ContaminationCheck = class {
|
|
|
2613
2613
|
//#region packages/sdlc/dist/verification/checks/test-mutation-check.js
|
|
2614
2614
|
/** Heuristic: is this path a test file? Covers the JS/TS/Python/Go idioms. */
|
|
2615
2615
|
function isTestPath(file) {
|
|
2616
|
-
const norm = file.replace(/\\/g, "/").toLowerCase();
|
|
2617
|
-
const base = norm.split("/").pop() ?? "";
|
|
2618
|
-
if (/(^|\/)(__tests__|tests?)\//.test(norm)) return true;
|
|
2616
|
+
const norm$1 = file.replace(/\\/g, "/").toLowerCase();
|
|
2617
|
+
const base = norm$1.split("/").pop() ?? "";
|
|
2618
|
+
if (/(^|\/)(__tests__|tests?)\//.test(norm$1)) return true;
|
|
2619
2619
|
if (/\.(test|spec)\.[a-z]+$/.test(base)) return true;
|
|
2620
2620
|
if (/^test_.*\.py$/.test(base) || /_test\.(py|go)$/.test(base)) return true;
|
|
2621
2621
|
if (base === "conftest.py") return true;
|
|
2622
|
-
if (/(^|\/)(fixtures|factories|testsupport|test-support|test_support|__mocks__|__fixtures__)\//.test(norm)) return true;
|
|
2622
|
+
if (/(^|\/)(fixtures|factories|testsupport|test-support|test_support|__mocks__|__fixtures__)\//.test(norm$1)) return true;
|
|
2623
2623
|
return false;
|
|
2624
2624
|
}
|
|
2625
2625
|
var TestMutationCheck = class {
|
|
@@ -2649,6 +2649,351 @@ var TestMutationCheck = class {
|
|
|
2649
2649
|
}
|
|
2650
2650
|
};
|
|
2651
2651
|
|
|
2652
|
+
//#endregion
|
|
2653
|
+
//#region packages/sdlc/dist/acceptance/registry.js
|
|
2654
|
+
/** Repo-relative path of the registry — the single canonical location. */
|
|
2655
|
+
const JOURNEY_REGISTRY_PATH = ".substrate/acceptance/journeys.yaml";
|
|
2656
|
+
const JourneySurfaceSchema = z.enum([
|
|
2657
|
+
"email",
|
|
2658
|
+
"cli",
|
|
2659
|
+
"file",
|
|
2660
|
+
"web"
|
|
2661
|
+
]);
|
|
2662
|
+
const JourneyEndStateSchema = z.object({
|
|
2663
|
+
id: z.string().min(1, "end-state id must be a non-empty string"),
|
|
2664
|
+
given: z.string().min(1, "given must be a non-empty string"),
|
|
2665
|
+
walk: z.string().min(1, "walk must be a non-empty string"),
|
|
2666
|
+
then: z.string().min(1, "then must be a non-empty string")
|
|
2667
|
+
});
|
|
2668
|
+
const JourneySchema = z.object({
|
|
2669
|
+
id: z.string().min(1, "journey id must be a non-empty string"),
|
|
2670
|
+
title: z.string().min(1, "title must be a non-empty string"),
|
|
2671
|
+
criticality: z.enum(["critical", "standard"]),
|
|
2672
|
+
surfaces: z.array(JourneySurfaceSchema).min(1, "a journey must declare at least one surface"),
|
|
2673
|
+
epic: z.number().int().positive().optional(),
|
|
2674
|
+
end_states: z.array(JourneyEndStateSchema).min(1, "a journey must declare at least one end_state — a journey with none is unjudgeable")
|
|
2675
|
+
});
|
|
2676
|
+
const JourneyRegistrySchema = z.object({
|
|
2677
|
+
version: z.number().int().positive("version must be a positive integer"),
|
|
2678
|
+
journeys: z.array(JourneySchema)
|
|
2679
|
+
}).superRefine((registry, ctx) => {
|
|
2680
|
+
const seenJourneyIds = new Map();
|
|
2681
|
+
registry.journeys.forEach((journey, i) => {
|
|
2682
|
+
const firstIndex = seenJourneyIds.get(journey.id);
|
|
2683
|
+
if (firstIndex !== void 0) ctx.addIssue({
|
|
2684
|
+
code: "custom",
|
|
2685
|
+
path: [
|
|
2686
|
+
"journeys",
|
|
2687
|
+
i,
|
|
2688
|
+
"id"
|
|
2689
|
+
],
|
|
2690
|
+
message: `duplicate journey id "${journey.id}" (first declared at journeys[${firstIndex}])`
|
|
2691
|
+
});
|
|
2692
|
+
else seenJourneyIds.set(journey.id, i);
|
|
2693
|
+
const seenEndStateIds = new Map();
|
|
2694
|
+
journey.end_states.forEach((endState, j) => {
|
|
2695
|
+
const firstEs = seenEndStateIds.get(endState.id);
|
|
2696
|
+
if (firstEs !== void 0) ctx.addIssue({
|
|
2697
|
+
code: "custom",
|
|
2698
|
+
path: [
|
|
2699
|
+
"journeys",
|
|
2700
|
+
i,
|
|
2701
|
+
"end_states",
|
|
2702
|
+
j,
|
|
2703
|
+
"id"
|
|
2704
|
+
],
|
|
2705
|
+
message: `duplicate end-state id "${endState.id}" in journey "${journey.id}" (first declared at end_states[${firstEs}])`
|
|
2706
|
+
});
|
|
2707
|
+
else seenEndStateIds.set(endState.id, j);
|
|
2708
|
+
});
|
|
2709
|
+
});
|
|
2710
|
+
});
|
|
2711
|
+
function zodErrorToValidationIssues(error) {
|
|
2712
|
+
return error.issues.map((issue) => ({
|
|
2713
|
+
path: issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)",
|
|
2714
|
+
message: issue.message
|
|
2715
|
+
}));
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Parse and validate registry YAML content.
|
|
2719
|
+
*
|
|
2720
|
+
* Never throws: malformed YAML, non-object documents, and schema violations
|
|
2721
|
+
* all come back as named, pathed issues.
|
|
2722
|
+
*/
|
|
2723
|
+
function parseJourneyRegistry(yamlContent) {
|
|
2724
|
+
let doc;
|
|
2725
|
+
try {
|
|
2726
|
+
doc = load(yamlContent);
|
|
2727
|
+
} catch (err) {
|
|
2728
|
+
const message = err instanceof YAMLException ? err.message : String(err);
|
|
2729
|
+
return {
|
|
2730
|
+
ok: false,
|
|
2731
|
+
issues: [{
|
|
2732
|
+
path: "(root)",
|
|
2733
|
+
message: `malformed YAML: ${message}`
|
|
2734
|
+
}]
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
if (doc === null || doc === void 0) return {
|
|
2738
|
+
ok: false,
|
|
2739
|
+
issues: [{
|
|
2740
|
+
path: "(root)",
|
|
2741
|
+
message: "registry file is empty"
|
|
2742
|
+
}]
|
|
2743
|
+
};
|
|
2744
|
+
const result = JourneyRegistrySchema.safeParse(doc);
|
|
2745
|
+
if (!result.success) return {
|
|
2746
|
+
ok: false,
|
|
2747
|
+
issues: zodErrorToValidationIssues(result.error)
|
|
2748
|
+
};
|
|
2749
|
+
return {
|
|
2750
|
+
ok: true,
|
|
2751
|
+
registry: result.data
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
//#endregion
|
|
2756
|
+
//#region packages/sdlc/dist/acceptance/coverage.js
|
|
2757
|
+
/**
|
|
2758
|
+
* Compute the coverage state of every journey audited at this scope.
|
|
2759
|
+
*
|
|
2760
|
+
* - `{epic: n}` audits journeys declaring `epic: n`.
|
|
2761
|
+
* - `{final: true}` audits the FULL registry (epicless journeys have no
|
|
2762
|
+
* earlier audit point; epic-scoped ones are re-audited — idempotent).
|
|
2763
|
+
*
|
|
2764
|
+
* State precedence: deferred > walked-fail > walked-pass > unwalked/unclaimed.
|
|
2765
|
+
* An operator deferral wins over everything (explicit scope decision); a fail
|
|
2766
|
+
* verdict wins over a pass (any failing end-state fails the journey).
|
|
2767
|
+
*/
|
|
2768
|
+
function computeJourneyCoverage(input) {
|
|
2769
|
+
const { registry, claims, verdicts, deferredJourneyIds, scope } = input;
|
|
2770
|
+
const audited = registry.journeys.filter((j) => "final" in scope ? true : j.epic === scope.epic);
|
|
2771
|
+
const claimsByJourney = new Map();
|
|
2772
|
+
for (const claim of claims) {
|
|
2773
|
+
const owners = claimsByJourney.get(claim.journeyId) ?? [];
|
|
2774
|
+
if (!owners.includes(claim.storyKey)) owners.push(claim.storyKey);
|
|
2775
|
+
claimsByJourney.set(claim.journeyId, owners);
|
|
2776
|
+
}
|
|
2777
|
+
const deferred = new Set(deferredJourneyIds);
|
|
2778
|
+
const verdictByJourney = new Map();
|
|
2779
|
+
for (const v of verdicts) {
|
|
2780
|
+
const existing = verdictByJourney.get(v.journeyId);
|
|
2781
|
+
verdictByJourney.set(v.journeyId, existing === "fail" ? "fail" : v.verdict);
|
|
2782
|
+
}
|
|
2783
|
+
return audited.map((journey) => {
|
|
2784
|
+
const ownerStories = claimsByJourney.get(journey.id) ?? [];
|
|
2785
|
+
let state;
|
|
2786
|
+
if (deferred.has(journey.id)) state = "deferred";
|
|
2787
|
+
else {
|
|
2788
|
+
const verdict = verdictByJourney.get(journey.id);
|
|
2789
|
+
if (verdict === "fail") state = "walked-fail";
|
|
2790
|
+
else if (verdict === "pass") state = "walked-pass";
|
|
2791
|
+
else state = ownerStories.length > 0 ? "unwalked" : "unclaimed";
|
|
2792
|
+
}
|
|
2793
|
+
return {
|
|
2794
|
+
journeyId: journey.id,
|
|
2795
|
+
title: journey.title,
|
|
2796
|
+
criticality: journey.criticality,
|
|
2797
|
+
...journey.epic !== void 0 ? { epic: journey.epic } : {},
|
|
2798
|
+
state,
|
|
2799
|
+
ownerStories
|
|
2800
|
+
};
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
/** Aggregate counts for the `acceptance:coverage` event payload. */
|
|
2804
|
+
function summarizeCoverage(entries) {
|
|
2805
|
+
const summary = {
|
|
2806
|
+
"walked-pass": 0,
|
|
2807
|
+
"walked-fail": 0,
|
|
2808
|
+
deferred: 0,
|
|
2809
|
+
unclaimed: 0,
|
|
2810
|
+
unwalked: 0
|
|
2811
|
+
};
|
|
2812
|
+
for (const entry of entries) summary[entry.state] += 1;
|
|
2813
|
+
return summary;
|
|
2814
|
+
}
|
|
2815
|
+
/** Repo-relative path of the operator deferral file. */
|
|
2816
|
+
const JOURNEY_DEFERRALS_PATH = ".substrate/acceptance/deferrals.yaml";
|
|
2817
|
+
const JourneyDeferralSchema = z.object({
|
|
2818
|
+
journey: z.string().min(1),
|
|
2819
|
+
reason: z.string().min(1, "a deferral must carry an operator reason"),
|
|
2820
|
+
deferred_at: z.string().optional()
|
|
2821
|
+
});
|
|
2822
|
+
const JourneyDeferralsFileSchema = z.object({ deferrals: z.array(JourneyDeferralSchema) });
|
|
2823
|
+
/** Parse deferrals YAML. Never throws; malformed content comes back as issues. */
|
|
2824
|
+
function parseJourneyDeferrals(yamlContent) {
|
|
2825
|
+
let doc;
|
|
2826
|
+
try {
|
|
2827
|
+
doc = load(yamlContent);
|
|
2828
|
+
} catch (err) {
|
|
2829
|
+
const message = err instanceof YAMLException ? err.message : String(err);
|
|
2830
|
+
return {
|
|
2831
|
+
ok: false,
|
|
2832
|
+
issues: [{
|
|
2833
|
+
path: "(root)",
|
|
2834
|
+
message: `malformed YAML: ${message}`
|
|
2835
|
+
}]
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
if (doc === null || doc === void 0) return {
|
|
2839
|
+
ok: true,
|
|
2840
|
+
deferrals: []
|
|
2841
|
+
};
|
|
2842
|
+
const result = JourneyDeferralsFileSchema.safeParse(doc);
|
|
2843
|
+
if (!result.success) return {
|
|
2844
|
+
ok: false,
|
|
2845
|
+
issues: result.error.issues.map((issue) => ({
|
|
2846
|
+
path: issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)",
|
|
2847
|
+
message: issue.message
|
|
2848
|
+
}))
|
|
2849
|
+
};
|
|
2850
|
+
return {
|
|
2851
|
+
ok: true,
|
|
2852
|
+
deferrals: result.data.deferrals
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
//#endregion
|
|
2857
|
+
//#region packages/sdlc/dist/acceptance/contract.js
|
|
2858
|
+
/** The acceptance contract lives inside the project profile. */
|
|
2859
|
+
const ACCEPTANCE_CONTRACT_PROFILE_PATH = ".substrate/project-profile.yaml";
|
|
2860
|
+
const RenderSurfaceSchema = z.object({ render: z.string().min(1) }).strict();
|
|
2861
|
+
const WebSurfaceSchema = z.object({
|
|
2862
|
+
serve: z.string().min(1),
|
|
2863
|
+
ready: z.string().optional()
|
|
2864
|
+
}).strict();
|
|
2865
|
+
const AcceptanceContractSchema = z.object({
|
|
2866
|
+
fixtures: z.string().optional(),
|
|
2867
|
+
surfaces: z.object({
|
|
2868
|
+
email: RenderSurfaceSchema.optional(),
|
|
2869
|
+
cli: RenderSurfaceSchema.optional(),
|
|
2870
|
+
file: RenderSurfaceSchema.optional(),
|
|
2871
|
+
web: WebSurfaceSchema.optional()
|
|
2872
|
+
}).strict()
|
|
2873
|
+
}).strict();
|
|
2874
|
+
/**
|
|
2875
|
+
* Extract + validate the `acceptance:` block from project-profile YAML
|
|
2876
|
+
* content. `absent` when the profile has no block (legal — the coverage
|
|
2877
|
+
* audit escalates registry-present+contract-absent as acceptance-unrunnable
|
|
2878
|
+
* in blocking mode; pure accounting still works without one).
|
|
2879
|
+
*/
|
|
2880
|
+
function parseAcceptanceContract(profileYamlContent) {
|
|
2881
|
+
let doc;
|
|
2882
|
+
try {
|
|
2883
|
+
doc = load(profileYamlContent);
|
|
2884
|
+
} catch (err) {
|
|
2885
|
+
const message = err instanceof YAMLException ? err.message : String(err);
|
|
2886
|
+
return {
|
|
2887
|
+
status: "invalid",
|
|
2888
|
+
issues: [{
|
|
2889
|
+
path: "(root)",
|
|
2890
|
+
message: `malformed profile YAML: ${message}`
|
|
2891
|
+
}]
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
if (doc === null || doc === void 0 || typeof doc !== "object") return { status: "absent" };
|
|
2895
|
+
const block = doc["acceptance"];
|
|
2896
|
+
if (block === void 0 || block === null) return { status: "absent" };
|
|
2897
|
+
const result = AcceptanceContractSchema.safeParse(block);
|
|
2898
|
+
if (!result.success) return {
|
|
2899
|
+
status: "invalid",
|
|
2900
|
+
issues: result.error.issues.map((issue) => ({
|
|
2901
|
+
path: issue.path.length > 0 ? `acceptance.${issue.path.map(String).join(".")}` : "acceptance",
|
|
2902
|
+
message: issue.message
|
|
2903
|
+
}))
|
|
2904
|
+
};
|
|
2905
|
+
return {
|
|
2906
|
+
status: "ok",
|
|
2907
|
+
contract: result.data
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
//#endregion
|
|
2912
|
+
//#region packages/sdlc/dist/verification/checks/acceptance-spec-check.js
|
|
2913
|
+
async function readWorktreeCopy(workingDir, relPath) {
|
|
2914
|
+
try {
|
|
2915
|
+
return await readFile$1(join$1(workingDir, relPath), "utf-8");
|
|
2916
|
+
} catch {
|
|
2917
|
+
return null;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
const norm = (s) => s === null ? null : s.replace(/\r\n/g, "\n").trim();
|
|
2921
|
+
/** Compare only the acceptance BLOCK of the profile (other profile keys are ContaminationCheck/H7 territory). */
|
|
2922
|
+
function acceptanceBlockFingerprint(profileContent) {
|
|
2923
|
+
if (profileContent === null) return "absent";
|
|
2924
|
+
const parsed = parseAcceptanceContract(profileContent);
|
|
2925
|
+
if (parsed.status === "absent") return "absent";
|
|
2926
|
+
if (parsed.status === "invalid") return "invalid:" + JSON.stringify(parsed.issues);
|
|
2927
|
+
return JSON.stringify(parsed.contract);
|
|
2928
|
+
}
|
|
2929
|
+
var AcceptanceSpecCheck = class {
|
|
2930
|
+
name = "acceptance-spec";
|
|
2931
|
+
tier = "A";
|
|
2932
|
+
async run(context) {
|
|
2933
|
+
const start = Date.now();
|
|
2934
|
+
const guard = context.acceptanceSpecGuard;
|
|
2935
|
+
if (guard === void 0) return {
|
|
2936
|
+
status: "pass",
|
|
2937
|
+
details: "no acceptance spec guard in context (acceptance not configured or non-worktree dispatch)",
|
|
2938
|
+
duration_ms: Date.now() - start,
|
|
2939
|
+
findings: []
|
|
2940
|
+
};
|
|
2941
|
+
const findings = [];
|
|
2942
|
+
const rawSpecs = [{
|
|
2943
|
+
path: JOURNEY_REGISTRY_PATH,
|
|
2944
|
+
trusted: guard.journeysTrusted
|
|
2945
|
+
}, {
|
|
2946
|
+
path: JOURNEY_DEFERRALS_PATH,
|
|
2947
|
+
trusted: guard.deferralsTrusted
|
|
2948
|
+
}];
|
|
2949
|
+
for (const spec of rawSpecs) {
|
|
2950
|
+
const worktree = await readWorktreeCopy(context.workingDir, spec.path);
|
|
2951
|
+
const trustedNorm = norm(spec.trusted);
|
|
2952
|
+
const worktreeNorm = norm(worktree);
|
|
2953
|
+
if (trustedNorm === worktreeNorm) continue;
|
|
2954
|
+
const shape = trustedNorm === null ? "INTRODUCED in the story worktree (acceptance specs are planning/operator-authored, never story output)" : worktreeNorm === null ? "DELETED in the story worktree" : "DIVERGES from the trusted copy";
|
|
2955
|
+
findings.push({
|
|
2956
|
+
category: "acceptance-spec-tampered",
|
|
2957
|
+
severity: "error",
|
|
2958
|
+
message: `${spec.path} ${shape}. The acceptance gate reads the trusted tree, so this edit cannot fool the current audit — but merging it would change every FUTURE audit. Revert it or make the change through planning (operator commit outside a story).`
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
const worktreeProfile = await readWorktreeCopy(context.workingDir, ACCEPTANCE_CONTRACT_PROFILE_PATH);
|
|
2962
|
+
const trustedFp = acceptanceBlockFingerprint(guard.profileTrusted);
|
|
2963
|
+
const worktreeFp = acceptanceBlockFingerprint(worktreeProfile);
|
|
2964
|
+
if (trustedFp !== worktreeFp) findings.push({
|
|
2965
|
+
category: "acceptance-spec-tampered",
|
|
2966
|
+
severity: "error",
|
|
2967
|
+
message: `the acceptance: contract block in ${ACCEPTANCE_CONTRACT_PROFILE_PATH} differs between the story worktree and the trusted tree (render-command laundering shape). Revert the worktree edit.`
|
|
2968
|
+
});
|
|
2969
|
+
if (guard.fixturesPath !== void 0) {
|
|
2970
|
+
const prefix = guard.fixturesPath.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
|
|
2971
|
+
const touched = (context.changedFiles ?? []).filter((f) => {
|
|
2972
|
+
const n = f.replace(/\\/g, "/");
|
|
2973
|
+
return n.startsWith(prefix) || n === prefix.slice(0, -1);
|
|
2974
|
+
});
|
|
2975
|
+
if (touched.length > 0) findings.push({
|
|
2976
|
+
category: "acceptance-fixture-mutation",
|
|
2977
|
+
severity: "warn",
|
|
2978
|
+
message: `story modified ${String(touched.length)} file(s) under the declared acceptance fixtures path (${guard.fixturesPath}): ${touched.slice(0, 10).join(", ")}` + (touched.length > 10 ? ` (+${String(touched.length - 10)} more)` : "") + `. The judge grounds verdicts in these fixtures — review whether the change tilts the walk.`
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
if (findings.length === 0) return {
|
|
2982
|
+
status: "pass",
|
|
2983
|
+
details: "acceptance specs match the trusted tree; no fixture mutations",
|
|
2984
|
+
duration_ms: Date.now() - start,
|
|
2985
|
+
findings: []
|
|
2986
|
+
};
|
|
2987
|
+
const hasError = findings.some((f) => f.severity === "error");
|
|
2988
|
+
return {
|
|
2989
|
+
status: hasError ? "fail" : "warn",
|
|
2990
|
+
details: renderFindings(findings),
|
|
2991
|
+
duration_ms: Date.now() - start,
|
|
2992
|
+
findings
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2996
|
+
|
|
2652
2997
|
//#endregion
|
|
2653
2998
|
//#region packages/sdlc/dist/verification/probes/types.js
|
|
2654
2999
|
/**
|
|
@@ -4604,6 +4949,7 @@ function createDefaultVerificationPipeline(bus, config) {
|
|
|
4604
4949
|
new TestSuiteCheck(),
|
|
4605
4950
|
new ContaminationCheck(),
|
|
4606
4951
|
new TestMutationCheck(),
|
|
4952
|
+
new AcceptanceSpecCheck(),
|
|
4607
4953
|
new RuntimeProbeCheck(),
|
|
4608
4954
|
new SourceAcFidelityCheck(),
|
|
4609
4955
|
new SourceAcShelloutCheck(),
|
|
@@ -4844,396 +5190,137 @@ function tokenize(s) {
|
|
|
4844
5190
|
return new Set(words);
|
|
4845
5191
|
}
|
|
4846
5192
|
/**
|
|
4847
|
-
* Compute Jaccard-based word overlap between two strings.
|
|
4848
|
-
* Returns 0 when either string has no tokens.
|
|
4849
|
-
*/
|
|
4850
|
-
function wordOverlap(a, b) {
|
|
4851
|
-
const setA = tokenize(a);
|
|
4852
|
-
const setB = tokenize(b);
|
|
4853
|
-
if (setA.size === 0 || setB.size === 0) return 0;
|
|
4854
|
-
let intersection = 0;
|
|
4855
|
-
for (const w of setA) if (setB.has(w)) intersection++;
|
|
4856
|
-
const union = new Set([...setA, ...setB]).size;
|
|
4857
|
-
return intersection / union;
|
|
4858
|
-
}
|
|
4859
|
-
/**
|
|
4860
|
-
* Extract numbered AC items from the `## Acceptance Criteria` section of a
|
|
4861
|
-
* story spec. Handles both plain `## Acceptance Criteria` headings and the
|
|
4862
|
-
* bold-wrapped variant `**Acceptance Criteria:**`.
|
|
4863
|
-
*
|
|
4864
|
-
* Stops scanning at the next `##` heading (or end-of-file).
|
|
4865
|
-
*
|
|
4866
|
-
* Returns an array of trimmed AC text strings (numbering stripped).
|
|
4867
|
-
*/
|
|
4868
|
-
function parseAcList(storyContent) {
|
|
4869
|
-
const lines = storyContent.split("\n");
|
|
4870
|
-
const items = [];
|
|
4871
|
-
let inAcSection = false;
|
|
4872
|
-
for (const line of lines) {
|
|
4873
|
-
if (/^##\s+Acceptance Criteria/i.test(line) || /\*\*Acceptance Criteria:\*\*/i.test(line)) {
|
|
4874
|
-
inAcSection = true;
|
|
4875
|
-
continue;
|
|
4876
|
-
}
|
|
4877
|
-
if (inAcSection && /^##\s/.test(line)) break;
|
|
4878
|
-
if (!inAcSection) continue;
|
|
4879
|
-
const numbered = line.match(/^\s*(?:\d+[.):]|AC\d+[.):])\s*(.+)/);
|
|
4880
|
-
if (numbered) {
|
|
4881
|
-
const text = (numbered[1] ?? "").trim();
|
|
4882
|
-
if (text.length > 0) items.push(text);
|
|
4883
|
-
}
|
|
4884
|
-
}
|
|
4885
|
-
return items;
|
|
4886
|
-
}
|
|
4887
|
-
const TEST_FILE_PATTERNS = [
|
|
4888
|
-
/\.test\.ts$/,
|
|
4889
|
-
/\.test\.js$/,
|
|
4890
|
-
/test/i
|
|
4891
|
-
];
|
|
4892
|
-
/** Returns true when the path matches a known test file pattern. */
|
|
4893
|
-
function isTestFile(path$3) {
|
|
4894
|
-
return TEST_FILE_PATTERNS.some((p) => p.test(path$3));
|
|
4895
|
-
}
|
|
4896
|
-
const TEST_DESC_RE = /(?:describe|it|test)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
4897
|
-
/**
|
|
4898
|
-
* Extract all string literals passed to `describe(`, `it(`, or `test(` in the
|
|
4899
|
-
* provided source text.
|
|
4900
|
-
*/
|
|
4901
|
-
function extractTestDescriptions(source) {
|
|
4902
|
-
const results = [];
|
|
4903
|
-
let m;
|
|
4904
|
-
TEST_DESC_RE.lastIndex = 0;
|
|
4905
|
-
while ((m = TEST_DESC_RE.exec(source)) !== null) {
|
|
4906
|
-
const desc = m[1];
|
|
4907
|
-
if (desc !== void 0 && desc.trim().length > 0) results.push(desc.trim());
|
|
4908
|
-
}
|
|
4909
|
-
return results;
|
|
4910
|
-
}
|
|
4911
|
-
/**
|
|
4912
|
-
* Run the AC-to-test traceability heuristic.
|
|
4913
|
-
*
|
|
4914
|
-
* Steps:
|
|
4915
|
-
* 1. Parse AC list from `input.storyContent`.
|
|
4916
|
-
* 2. Filter `input.filesModified` to test files.
|
|
4917
|
-
* 3. Read each test file and extract describe/it/test descriptions.
|
|
4918
|
-
* 4. For each AC × each test description, compute word-overlap score.
|
|
4919
|
-
* 5. A score ≥ 0.4 marks the AC as "matched".
|
|
4920
|
-
* 6. Return the matrix with `confidence: 'approximate'`.
|
|
4921
|
-
*
|
|
4922
|
-
* File I/O errors are silently ignored per file (the file is simply skipped).
|
|
4923
|
-
* When no test files are found, all ACs are "not matched" and a warning is emitted.
|
|
4924
|
-
*/
|
|
4925
|
-
async function runAcTraceabilityCheck(input) {
|
|
4926
|
-
const { storyKey, storyContent, filesModified } = input;
|
|
4927
|
-
const fileReader = input._readFile ?? ((p) => readFile(p, "utf-8"));
|
|
4928
|
-
const warnings = [];
|
|
4929
|
-
const acList = parseAcList(storyContent);
|
|
4930
|
-
const testFiles = filesModified.filter(isTestFile);
|
|
4931
|
-
if (testFiles.length === 0) {
|
|
4932
|
-
warnings.push(`No test files found in filesModified for story ${storyKey}. All ACs marked as unmatched.`);
|
|
4933
|
-
const matrix$1 = acList.map((acText) => ({
|
|
4934
|
-
acText,
|
|
4935
|
-
matched: false,
|
|
4936
|
-
testName: null,
|
|
4937
|
-
score: 0
|
|
4938
|
-
}));
|
|
4939
|
-
return {
|
|
4940
|
-
storyKey,
|
|
4941
|
-
matrix: matrix$1,
|
|
4942
|
-
confidence: "approximate",
|
|
4943
|
-
warnings
|
|
4944
|
-
};
|
|
4945
|
-
}
|
|
4946
|
-
const allTestDescriptions = [];
|
|
4947
|
-
for (const filePath of testFiles) try {
|
|
4948
|
-
const source = await fileReader(filePath);
|
|
4949
|
-
if (source != null) {
|
|
4950
|
-
const descs = extractTestDescriptions(source);
|
|
4951
|
-
allTestDescriptions.push(...descs);
|
|
4952
|
-
}
|
|
4953
|
-
} catch {}
|
|
4954
|
-
const MATCH_THRESHOLD = .4;
|
|
4955
|
-
const matrix = acList.map((acText) => {
|
|
4956
|
-
let bestScore = 0;
|
|
4957
|
-
let bestTestName = null;
|
|
4958
|
-
for (const desc of allTestDescriptions) {
|
|
4959
|
-
const score = wordOverlap(acText, desc);
|
|
4960
|
-
if (score > bestScore) {
|
|
4961
|
-
bestScore = score;
|
|
4962
|
-
bestTestName = desc;
|
|
4963
|
-
}
|
|
4964
|
-
}
|
|
4965
|
-
const matched = bestScore >= MATCH_THRESHOLD;
|
|
4966
|
-
return {
|
|
4967
|
-
acText,
|
|
4968
|
-
matched,
|
|
4969
|
-
testName: matched ? bestTestName : null,
|
|
4970
|
-
score: bestScore
|
|
4971
|
-
};
|
|
4972
|
-
});
|
|
4973
|
-
return {
|
|
4974
|
-
storyKey,
|
|
4975
|
-
matrix,
|
|
4976
|
-
confidence: "approximate",
|
|
4977
|
-
warnings
|
|
4978
|
-
};
|
|
4979
|
-
}
|
|
4980
|
-
|
|
4981
|
-
//#endregion
|
|
4982
|
-
//#region packages/sdlc/dist/acceptance/registry.js
|
|
4983
|
-
/** Repo-relative path of the registry — the single canonical location. */
|
|
4984
|
-
const JOURNEY_REGISTRY_PATH = ".substrate/acceptance/journeys.yaml";
|
|
4985
|
-
const JourneySurfaceSchema = z.enum([
|
|
4986
|
-
"email",
|
|
4987
|
-
"cli",
|
|
4988
|
-
"file",
|
|
4989
|
-
"web"
|
|
4990
|
-
]);
|
|
4991
|
-
const JourneyEndStateSchema = z.object({
|
|
4992
|
-
id: z.string().min(1, "end-state id must be a non-empty string"),
|
|
4993
|
-
given: z.string().min(1, "given must be a non-empty string"),
|
|
4994
|
-
walk: z.string().min(1, "walk must be a non-empty string"),
|
|
4995
|
-
then: z.string().min(1, "then must be a non-empty string")
|
|
4996
|
-
});
|
|
4997
|
-
const JourneySchema = z.object({
|
|
4998
|
-
id: z.string().min(1, "journey id must be a non-empty string"),
|
|
4999
|
-
title: z.string().min(1, "title must be a non-empty string"),
|
|
5000
|
-
criticality: z.enum(["critical", "standard"]),
|
|
5001
|
-
surfaces: z.array(JourneySurfaceSchema).min(1, "a journey must declare at least one surface"),
|
|
5002
|
-
epic: z.number().int().positive().optional(),
|
|
5003
|
-
end_states: z.array(JourneyEndStateSchema).min(1, "a journey must declare at least one end_state — a journey with none is unjudgeable")
|
|
5004
|
-
});
|
|
5005
|
-
const JourneyRegistrySchema = z.object({
|
|
5006
|
-
version: z.number().int().positive("version must be a positive integer"),
|
|
5007
|
-
journeys: z.array(JourneySchema)
|
|
5008
|
-
}).superRefine((registry, ctx) => {
|
|
5009
|
-
const seenJourneyIds = new Map();
|
|
5010
|
-
registry.journeys.forEach((journey, i) => {
|
|
5011
|
-
const firstIndex = seenJourneyIds.get(journey.id);
|
|
5012
|
-
if (firstIndex !== void 0) ctx.addIssue({
|
|
5013
|
-
code: "custom",
|
|
5014
|
-
path: [
|
|
5015
|
-
"journeys",
|
|
5016
|
-
i,
|
|
5017
|
-
"id"
|
|
5018
|
-
],
|
|
5019
|
-
message: `duplicate journey id "${journey.id}" (first declared at journeys[${firstIndex}])`
|
|
5020
|
-
});
|
|
5021
|
-
else seenJourneyIds.set(journey.id, i);
|
|
5022
|
-
const seenEndStateIds = new Map();
|
|
5023
|
-
journey.end_states.forEach((endState, j) => {
|
|
5024
|
-
const firstEs = seenEndStateIds.get(endState.id);
|
|
5025
|
-
if (firstEs !== void 0) ctx.addIssue({
|
|
5026
|
-
code: "custom",
|
|
5027
|
-
path: [
|
|
5028
|
-
"journeys",
|
|
5029
|
-
i,
|
|
5030
|
-
"end_states",
|
|
5031
|
-
j,
|
|
5032
|
-
"id"
|
|
5033
|
-
],
|
|
5034
|
-
message: `duplicate end-state id "${endState.id}" in journey "${journey.id}" (first declared at end_states[${firstEs}])`
|
|
5035
|
-
});
|
|
5036
|
-
else seenEndStateIds.set(endState.id, j);
|
|
5037
|
-
});
|
|
5038
|
-
});
|
|
5039
|
-
});
|
|
5040
|
-
function zodErrorToValidationIssues(error) {
|
|
5041
|
-
return error.issues.map((issue) => ({
|
|
5042
|
-
path: issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)",
|
|
5043
|
-
message: issue.message
|
|
5044
|
-
}));
|
|
5045
|
-
}
|
|
5046
|
-
/**
|
|
5047
|
-
* Parse and validate registry YAML content.
|
|
5048
|
-
*
|
|
5049
|
-
* Never throws: malformed YAML, non-object documents, and schema violations
|
|
5050
|
-
* all come back as named, pathed issues.
|
|
5051
|
-
*/
|
|
5052
|
-
function parseJourneyRegistry(yamlContent) {
|
|
5053
|
-
let doc;
|
|
5054
|
-
try {
|
|
5055
|
-
doc = load(yamlContent);
|
|
5056
|
-
} catch (err) {
|
|
5057
|
-
const message = err instanceof YAMLException ? err.message : String(err);
|
|
5058
|
-
return {
|
|
5059
|
-
ok: false,
|
|
5060
|
-
issues: [{
|
|
5061
|
-
path: "(root)",
|
|
5062
|
-
message: `malformed YAML: ${message}`
|
|
5063
|
-
}]
|
|
5064
|
-
};
|
|
5065
|
-
}
|
|
5066
|
-
if (doc === null || doc === void 0) return {
|
|
5067
|
-
ok: false,
|
|
5068
|
-
issues: [{
|
|
5069
|
-
path: "(root)",
|
|
5070
|
-
message: "registry file is empty"
|
|
5071
|
-
}]
|
|
5072
|
-
};
|
|
5073
|
-
const result = JourneyRegistrySchema.safeParse(doc);
|
|
5074
|
-
if (!result.success) return {
|
|
5075
|
-
ok: false,
|
|
5076
|
-
issues: zodErrorToValidationIssues(result.error)
|
|
5077
|
-
};
|
|
5078
|
-
return {
|
|
5079
|
-
ok: true,
|
|
5080
|
-
registry: result.data
|
|
5081
|
-
};
|
|
5082
|
-
}
|
|
5083
|
-
|
|
5084
|
-
//#endregion
|
|
5085
|
-
//#region packages/sdlc/dist/acceptance/coverage.js
|
|
5086
|
-
/**
|
|
5087
|
-
* Compute the coverage state of every journey audited at this scope.
|
|
5193
|
+
* Compute Jaccard-based word overlap between two strings.
|
|
5194
|
+
* Returns 0 when either string has no tokens.
|
|
5195
|
+
*/
|
|
5196
|
+
function wordOverlap(a, b) {
|
|
5197
|
+
const setA = tokenize(a);
|
|
5198
|
+
const setB = tokenize(b);
|
|
5199
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
5200
|
+
let intersection = 0;
|
|
5201
|
+
for (const w of setA) if (setB.has(w)) intersection++;
|
|
5202
|
+
const union = new Set([...setA, ...setB]).size;
|
|
5203
|
+
return intersection / union;
|
|
5204
|
+
}
|
|
5205
|
+
/**
|
|
5206
|
+
* Extract numbered AC items from the `## Acceptance Criteria` section of a
|
|
5207
|
+
* story spec. Handles both plain `## Acceptance Criteria` headings and the
|
|
5208
|
+
* bold-wrapped variant `**Acceptance Criteria:**`.
|
|
5088
5209
|
*
|
|
5089
|
-
*
|
|
5090
|
-
* - `{final: true}` audits the FULL registry (epicless journeys have no
|
|
5091
|
-
* earlier audit point; epic-scoped ones are re-audited — idempotent).
|
|
5210
|
+
* Stops scanning at the next `##` heading (or end-of-file).
|
|
5092
5211
|
*
|
|
5093
|
-
*
|
|
5094
|
-
* An operator deferral wins over everything (explicit scope decision); a fail
|
|
5095
|
-
* verdict wins over a pass (any failing end-state fails the journey).
|
|
5212
|
+
* Returns an array of trimmed AC text strings (numbering stripped).
|
|
5096
5213
|
*/
|
|
5097
|
-
function
|
|
5098
|
-
const
|
|
5099
|
-
const
|
|
5100
|
-
|
|
5101
|
-
for (const
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
}
|
|
5106
|
-
const deferred = new Set(deferredJourneyIds);
|
|
5107
|
-
const verdictByJourney = new Map();
|
|
5108
|
-
for (const v of verdicts) {
|
|
5109
|
-
const existing = verdictByJourney.get(v.journeyId);
|
|
5110
|
-
verdictByJourney.set(v.journeyId, existing === "fail" ? "fail" : v.verdict);
|
|
5111
|
-
}
|
|
5112
|
-
return audited.map((journey) => {
|
|
5113
|
-
const ownerStories = claimsByJourney.get(journey.id) ?? [];
|
|
5114
|
-
let state;
|
|
5115
|
-
if (deferred.has(journey.id)) state = "deferred";
|
|
5116
|
-
else {
|
|
5117
|
-
const verdict = verdictByJourney.get(journey.id);
|
|
5118
|
-
if (verdict === "fail") state = "walked-fail";
|
|
5119
|
-
else if (verdict === "pass") state = "walked-pass";
|
|
5120
|
-
else state = ownerStories.length > 0 ? "unwalked" : "unclaimed";
|
|
5214
|
+
function parseAcList(storyContent) {
|
|
5215
|
+
const lines = storyContent.split("\n");
|
|
5216
|
+
const items = [];
|
|
5217
|
+
let inAcSection = false;
|
|
5218
|
+
for (const line of lines) {
|
|
5219
|
+
if (/^##\s+Acceptance Criteria/i.test(line) || /\*\*Acceptance Criteria:\*\*/i.test(line)) {
|
|
5220
|
+
inAcSection = true;
|
|
5221
|
+
continue;
|
|
5121
5222
|
}
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5223
|
+
if (inAcSection && /^##\s/.test(line)) break;
|
|
5224
|
+
if (!inAcSection) continue;
|
|
5225
|
+
const numbered = line.match(/^\s*(?:\d+[.):]|AC\d+[.):])\s*(.+)/);
|
|
5226
|
+
if (numbered) {
|
|
5227
|
+
const text = (numbered[1] ?? "").trim();
|
|
5228
|
+
if (text.length > 0) items.push(text);
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
return items;
|
|
5131
5232
|
}
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
};
|
|
5141
|
-
for (const entry of entries) summary[entry.state] += 1;
|
|
5142
|
-
return summary;
|
|
5233
|
+
const TEST_FILE_PATTERNS = [
|
|
5234
|
+
/\.test\.ts$/,
|
|
5235
|
+
/\.test\.js$/,
|
|
5236
|
+
/test/i
|
|
5237
|
+
];
|
|
5238
|
+
/** Returns true when the path matches a known test file pattern. */
|
|
5239
|
+
function isTestFile(path$3) {
|
|
5240
|
+
return TEST_FILE_PATTERNS.some((p) => p.test(path$3));
|
|
5143
5241
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
doc = load(yamlContent);
|
|
5157
|
-
} catch (err) {
|
|
5158
|
-
const message = err instanceof YAMLException ? err.message : String(err);
|
|
5159
|
-
return {
|
|
5160
|
-
ok: false,
|
|
5161
|
-
issues: [{
|
|
5162
|
-
path: "(root)",
|
|
5163
|
-
message: `malformed YAML: ${message}`
|
|
5164
|
-
}]
|
|
5165
|
-
};
|
|
5242
|
+
const TEST_DESC_RE = /(?:describe|it|test)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
5243
|
+
/**
|
|
5244
|
+
* Extract all string literals passed to `describe(`, `it(`, or `test(` in the
|
|
5245
|
+
* provided source text.
|
|
5246
|
+
*/
|
|
5247
|
+
function extractTestDescriptions(source) {
|
|
5248
|
+
const results = [];
|
|
5249
|
+
let m;
|
|
5250
|
+
TEST_DESC_RE.lastIndex = 0;
|
|
5251
|
+
while ((m = TEST_DESC_RE.exec(source)) !== null) {
|
|
5252
|
+
const desc = m[1];
|
|
5253
|
+
if (desc !== void 0 && desc.trim().length > 0) results.push(desc.trim());
|
|
5166
5254
|
}
|
|
5167
|
-
|
|
5168
|
-
ok: true,
|
|
5169
|
-
deferrals: []
|
|
5170
|
-
};
|
|
5171
|
-
const result = JourneyDeferralsFileSchema.safeParse(doc);
|
|
5172
|
-
if (!result.success) return {
|
|
5173
|
-
ok: false,
|
|
5174
|
-
issues: result.error.issues.map((issue) => ({
|
|
5175
|
-
path: issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)",
|
|
5176
|
-
message: issue.message
|
|
5177
|
-
}))
|
|
5178
|
-
};
|
|
5179
|
-
return {
|
|
5180
|
-
ok: true,
|
|
5181
|
-
deferrals: result.data.deferrals
|
|
5182
|
-
};
|
|
5255
|
+
return results;
|
|
5183
5256
|
}
|
|
5184
|
-
|
|
5185
|
-
//#endregion
|
|
5186
|
-
//#region packages/sdlc/dist/acceptance/contract.js
|
|
5187
|
-
/** The acceptance contract lives inside the project profile. */
|
|
5188
|
-
const ACCEPTANCE_CONTRACT_PROFILE_PATH = ".substrate/project-profile.yaml";
|
|
5189
|
-
const RenderSurfaceSchema = z.object({ render: z.string().min(1) }).strict();
|
|
5190
|
-
const WebSurfaceSchema = z.object({
|
|
5191
|
-
serve: z.string().min(1),
|
|
5192
|
-
ready: z.string().optional()
|
|
5193
|
-
}).strict();
|
|
5194
|
-
const AcceptanceContractSchema = z.object({
|
|
5195
|
-
fixtures: z.string().optional(),
|
|
5196
|
-
surfaces: z.object({
|
|
5197
|
-
email: RenderSurfaceSchema.optional(),
|
|
5198
|
-
cli: RenderSurfaceSchema.optional(),
|
|
5199
|
-
file: RenderSurfaceSchema.optional(),
|
|
5200
|
-
web: WebSurfaceSchema.optional()
|
|
5201
|
-
}).strict()
|
|
5202
|
-
}).strict();
|
|
5203
5257
|
/**
|
|
5204
|
-
*
|
|
5205
|
-
*
|
|
5206
|
-
*
|
|
5207
|
-
*
|
|
5258
|
+
* Run the AC-to-test traceability heuristic.
|
|
5259
|
+
*
|
|
5260
|
+
* Steps:
|
|
5261
|
+
* 1. Parse AC list from `input.storyContent`.
|
|
5262
|
+
* 2. Filter `input.filesModified` to test files.
|
|
5263
|
+
* 3. Read each test file and extract describe/it/test descriptions.
|
|
5264
|
+
* 4. For each AC × each test description, compute word-overlap score.
|
|
5265
|
+
* 5. A score ≥ 0.4 marks the AC as "matched".
|
|
5266
|
+
* 6. Return the matrix with `confidence: 'approximate'`.
|
|
5267
|
+
*
|
|
5268
|
+
* File I/O errors are silently ignored per file (the file is simply skipped).
|
|
5269
|
+
* When no test files are found, all ACs are "not matched" and a warning is emitted.
|
|
5208
5270
|
*/
|
|
5209
|
-
function
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5271
|
+
async function runAcTraceabilityCheck(input) {
|
|
5272
|
+
const { storyKey, storyContent, filesModified } = input;
|
|
5273
|
+
const fileReader = input._readFile ?? ((p) => readFile(p, "utf-8"));
|
|
5274
|
+
const warnings = [];
|
|
5275
|
+
const acList = parseAcList(storyContent);
|
|
5276
|
+
const testFiles = filesModified.filter(isTestFile);
|
|
5277
|
+
if (testFiles.length === 0) {
|
|
5278
|
+
warnings.push(`No test files found in filesModified for story ${storyKey}. All ACs marked as unmatched.`);
|
|
5279
|
+
const matrix$1 = acList.map((acText) => ({
|
|
5280
|
+
acText,
|
|
5281
|
+
matched: false,
|
|
5282
|
+
testName: null,
|
|
5283
|
+
score: 0
|
|
5284
|
+
}));
|
|
5215
5285
|
return {
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
}]
|
|
5286
|
+
storyKey,
|
|
5287
|
+
matrix: matrix$1,
|
|
5288
|
+
confidence: "approximate",
|
|
5289
|
+
warnings
|
|
5221
5290
|
};
|
|
5222
5291
|
}
|
|
5223
|
-
|
|
5224
|
-
const
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5292
|
+
const allTestDescriptions = [];
|
|
5293
|
+
for (const filePath of testFiles) try {
|
|
5294
|
+
const source = await fileReader(filePath);
|
|
5295
|
+
if (source != null) {
|
|
5296
|
+
const descs = extractTestDescriptions(source);
|
|
5297
|
+
allTestDescriptions.push(...descs);
|
|
5298
|
+
}
|
|
5299
|
+
} catch {}
|
|
5300
|
+
const MATCH_THRESHOLD = .4;
|
|
5301
|
+
const matrix = acList.map((acText) => {
|
|
5302
|
+
let bestScore = 0;
|
|
5303
|
+
let bestTestName = null;
|
|
5304
|
+
for (const desc of allTestDescriptions) {
|
|
5305
|
+
const score = wordOverlap(acText, desc);
|
|
5306
|
+
if (score > bestScore) {
|
|
5307
|
+
bestScore = score;
|
|
5308
|
+
bestTestName = desc;
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5311
|
+
const matched = bestScore >= MATCH_THRESHOLD;
|
|
5312
|
+
return {
|
|
5313
|
+
acText,
|
|
5314
|
+
matched,
|
|
5315
|
+
testName: matched ? bestTestName : null,
|
|
5316
|
+
score: bestScore
|
|
5317
|
+
};
|
|
5318
|
+
});
|
|
5234
5319
|
return {
|
|
5235
|
-
|
|
5236
|
-
|
|
5320
|
+
storyKey,
|
|
5321
|
+
matrix,
|
|
5322
|
+
confidence: "approximate",
|
|
5323
|
+
warnings
|
|
5237
5324
|
};
|
|
5238
5325
|
}
|
|
5239
5326
|
|
|
@@ -5740,8 +5827,19 @@ const RunManifestSchema = z.object({
|
|
|
5740
5827
|
"unclaimed",
|
|
5741
5828
|
"unwalked"
|
|
5742
5829
|
]),
|
|
5743
|
-
ownerStories: z.array(z.string())
|
|
5830
|
+
ownerStories: z.array(z.string()),
|
|
5831
|
+
verdicts: z.array(z.object({
|
|
5832
|
+
end_state_id: z.string(),
|
|
5833
|
+
verdict: z.enum([
|
|
5834
|
+
"PASS",
|
|
5835
|
+
"FAIL",
|
|
5836
|
+
"UNREACHABLE"
|
|
5837
|
+
]),
|
|
5838
|
+
artifact: z.string(),
|
|
5839
|
+
excerpt: z.string()
|
|
5840
|
+
})).optional()
|
|
5744
5841
|
})).optional(),
|
|
5842
|
+
acceptance_report_path: z.string().optional(),
|
|
5745
5843
|
generation: z.number().int().nonnegative(),
|
|
5746
5844
|
created_at: z.string(),
|
|
5747
5845
|
updated_at: z.string()
|
|
@@ -6823,4 +6921,4 @@ async function resolveRunManifest(dbRoot, runId) {
|
|
|
6823
6921
|
|
|
6824
6922
|
//#endregion
|
|
6825
6923
|
export { ACCEPTANCE_CONTRACT_PROFILE_PATH, FindingsInjector, JOURNEY_DEFERRALS_PATH, JOURNEY_REGISTRY_PATH, RunManifest, RuntimeProbeListSchema, SupervisorLock, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, aggregateProbeAuthorMetrics, applyConfigToGraph, computeJourneyCoverage, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, loadAcceptanceContractFromTrustedTree, loadJourneyDeferralsFromTrustedTree, loadJourneyRegistryFromFile, loadJourneyRegistryFromTrustedTree, parseAcceptanceContract, parseJourneyDeferrals, parseRuntimeProbes, parseStoryFrontmatter, readCurrentRunId, renderFindings, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runAcTraceabilityCheck, runStaleVerificationRecovery, summarizeCoverage };
|
|
6826
|
-
//# sourceMappingURL=manifest-read-
|
|
6924
|
+
//# sourceMappingURL=manifest-read-D7HcgX21.js.map
|