substrate-ai 0.21.4 → 0.21.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/dist/cli/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { FileKvStore, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createDatabaseAdapter, createDoltOperatorReader, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion } from "../health-BUl3y64N.js";
2
+ import { FileKvStore, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, buildPipelineStatusOutput, createDatabaseAdapter, createDoltOperatorReader, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion } from "../health-hYMxKqAH.js";
3
3
  import { createLogger } from "../logger-KeHncl-f.js";
4
4
  import { createEventBus } from "../helpers-CElYrONe.js";
5
5
  import { AdapterRegistry, BudgetConfigSchema, CURRENT_CONFIG_FORMAT_VERSION, CURRENT_TASK_GRAPH_VERSION, ConfigError, CostTrackerConfigSchema, DEFAULT_CONFIG, DoltClient, DoltNotInstalled, GlobalSettingsSchema, InMemoryDatabaseAdapter, IngestionServer, MonitorDatabaseImpl, OPERATIONAL_FINDING, PartialGlobalSettingsSchema, PartialProviderConfigSchema, ProvidersSchema, RoutingRecommender, STORY_METRICS, TelemetryConfigSchema, addTokenUsage, aggregateTokenUsageForRun, checkDoltInstalled, compareRunMetrics, createAmendmentRun, createConfigSystem, createDecision, createGitWorktreeManager, createPipelineRun, createStderrLogger, getActiveDecisions, getAllCostEntriesFiltered, getBaselineRunMetrics, getDecisionsByCategory, getDecisionsByPhaseForRun, getLatestCompletedRun, getLatestRun, getPipelineRunById, getPlanningCostTotal, getRetryableEscalations, getRunMetrics, getRunningPipelineRuns, getSessionCostSummary, getSessionCostSummaryFiltered, getStoryMetricsForRun, getTokenUsageSummary, incrementRunRestarts, initSchema, initWorkGraphSchema, initializeDolt, listRunMetrics, loadParentRunDecisions, supersedeDecision, swallowDebug, tagRunAsBaseline, updatePipelineRun } from "../dist-CpnLJuVw.js";
6
6
  import "../adapter-registry-DXLMTmfD.js";
7
- import { ACCEPTANCE_CONTRACT_PROFILE_PATH, JOURNEY_DEFERRALS_PATH, JOURNEY_REGISTRY_PATH, RunManifest, SupervisorLock, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, aggregateProbeAuthorMetrics, loadAcceptanceContractFromTrustedTree, loadJourneyRegistryFromFile, loadJourneyRegistryFromTrustedTree, parseAcceptanceContract, parseJourneyDeferrals, parseRuntimeProbes, readCurrentRunId, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runAcTraceabilityCheck } from "../manifest-read-DZDlSq_s.js";
8
- import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GLOBSTAR, GitClient, GrammarLoader, Minimatch, Minipass, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, escape, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runProbeAuthor, runSolutioningPhase, unescape, validateStopAfterFromConflict } from "../run-nSb1TD3o.js";
7
+ import { ACCEPTANCE_CONTRACT_PROFILE_PATH, JOURNEY_DEFERRALS_PATH, JOURNEY_REGISTRY_PATH, RunManifest, SupervisorLock, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, aggregateProbeAuthorMetrics, loadAcceptanceContractFromTrustedTree, loadJourneyRegistryFromFile, loadJourneyRegistryFromTrustedTree, parseAcceptanceContract, parseJourneyDeferrals, parseRuntimeProbes, readCurrentRunId, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runAcTraceabilityCheck } from "../manifest-read-C0N7b9nH.js";
8
+ import { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GLOBSTAR, GitClient, GrammarLoader, Minimatch, Minipass, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, escape, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runProbeAuthor, runSolutioningPhase, unescape, validateStopAfterFromConflict } from "../run-C7PLGWT0.js";
9
9
  import "../errors-8tVEwG6v.js";
10
10
  import "../routing-DFxoKHDt.js";
11
11
  import { WorkGraphRepository } from "../work-graph-repository-DZyJv5pV.js";
12
12
  import "../decisions-CzSIEeGP.js";
13
13
  import "../decision-router-BAPpON_C.js";
14
- import "../interactive-prompt-C4Is1QDB.js";
14
+ import "../interactive-prompt-BvY2FKsl.js";
15
15
  import "../recovery-engine-BKGBeBnW.js";
16
16
  import "../version-manager-impl-qFBiO4Eh.js";
17
17
  import { registerUpgradeCommand } from "../upgrade-BUSzXgnd.js";
@@ -7130,7 +7130,7 @@ async function runStatusAction(options) {
7130
7130
  logger$15.debug({ err }, "Work graph query failed, continuing without work graph data");
7131
7131
  }
7132
7132
  if (run === void 0) {
7133
- const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-DklP4Se9.js");
7133
+ const { inspectProcessTree: inspectProcessTree$1 } = await import("../health-BdYZpCP1.js");
7134
7134
  const substrateDirPath = join(projectRoot, ".substrate");
7135
7135
  const processInfo = inspectProcessTree$1({
7136
7136
  projectRoot,
@@ -8659,7 +8659,7 @@ async function runSupervisorAction(options, deps = {}) {
8659
8659
  await initSchema(expAdapter);
8660
8660
  const { runRunAction: runPipeline } = await import(
8661
8661
  /* @vite-ignore */
8662
- "../run-CMXmzqoy.js"
8662
+ "../run-CHMwDPRx.js"
8663
8663
  );
8664
8664
  const runStoryFn = async (opts) => {
8665
8665
  const exitCode = await runPipeline({
@@ -1,7 +1,7 @@
1
- import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-BUl3y64N.js";
1
+ import { DEFAULT_STALL_THRESHOLD_SECONDS, getAllDescendantPids, getAutoHealthData, inspectProcessTree, isOrchestratorProcessLine, registerHealthCommand, runHealthAction } from "./health-hYMxKqAH.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./dist-CpnLJuVw.js";
4
- import "./manifest-read-DZDlSq_s.js";
4
+ import "./manifest-read-C0N7b9nH.js";
5
5
  import "./work-graph-repository-DZyJv5pV.js";
6
6
  import "./decisions-CzSIEeGP.js";
7
7
 
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from "./logger-KeHncl-f.js";
2
2
  import { DoltClient, DoltQueryError, createDatabaseAdapter$1 as createDatabaseAdapter, getLatestRun, getPipelineRunById, initSchema } from "./dist-CpnLJuVw.js";
3
- import { resolveMainRepoRoot, resolveRunManifest } from "./manifest-read-DZDlSq_s.js";
3
+ import { resolveMainRepoRoot, resolveRunManifest } from "./manifest-read-C0N7b9nH.js";
4
4
  import { createRequire } from "module";
5
5
  import { dirname, join } from "path";
6
6
  import { existsSync, readFileSync } from "node:fs";
@@ -1000,4 +1000,4 @@ function registerHealthCommand(program, _version = "0.0.0", projectRoot = proces
1000
1000
 
1001
1001
  //#endregion
1002
1002
  export { BMAD_BASELINE_TOKENS_FULL, DEFAULT_STALL_THRESHOLD_SECONDS, FileKvStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, SUBSTRATE_OWNED_SETTINGS_KEYS, VALID_PHASES, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter$1 as createDatabaseAdapter, createDoltOperatorReader, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, isOrchestratorProcessLine, parseDbTimestampAsUtc, registerHealthCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, runHealthAction, validateStoryKey };
1003
- //# sourceMappingURL=health-BUl3y64N.js.map
1003
+ //# sourceMappingURL=health-hYMxKqAH.js.map
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "./logger-KeHncl-f.js";
2
- import { readCurrentRunId, resolveMainRepoRoot } from "./manifest-read-DZDlSq_s.js";
2
+ import { readCurrentRunId, resolveMainRepoRoot } from "./manifest-read-C0N7b9nH.js";
3
3
  import { join } from "node:path";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import * as readline from "node:readline";
@@ -180,4 +180,4 @@ async function runInteractivePrompt(decisionContext) {
180
180
 
181
181
  //#endregion
182
182
  export { runInteractivePrompt };
183
- //# sourceMappingURL=interactive-prompt-C4Is1QDB.js.map
183
+ //# sourceMappingURL=interactive-prompt-BvY2FKsl.js.map
@@ -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
- * - `{epic: n}` audits journeys declaring `epic: n`.
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
- * State precedence: deferred > walked-fail > walked-pass > unwalked/unclaimed.
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 computeJourneyCoverage(input) {
5098
- const { registry, claims, verdicts, deferredJourneyIds, scope } = input;
5099
- const audited = registry.journeys.filter((j) => "final" in scope ? true : j.epic === scope.epic);
5100
- const claimsByJourney = new Map();
5101
- for (const claim of claims) {
5102
- const owners = claimsByJourney.get(claim.journeyId) ?? [];
5103
- if (!owners.includes(claim.storyKey)) owners.push(claim.storyKey);
5104
- claimsByJourney.set(claim.journeyId, owners);
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
- return {
5123
- journeyId: journey.id,
5124
- title: journey.title,
5125
- criticality: journey.criticality,
5126
- ...journey.epic !== void 0 ? { epic: journey.epic } : {},
5127
- state,
5128
- ownerStories
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
- /** Aggregate counts for the `acceptance:coverage` event payload. */
5133
- function summarizeCoverage(entries) {
5134
- const summary = {
5135
- "walked-pass": 0,
5136
- "walked-fail": 0,
5137
- deferred: 0,
5138
- unclaimed: 0,
5139
- unwalked: 0
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
- /** Repo-relative path of the operator deferral file. */
5145
- const JOURNEY_DEFERRALS_PATH = ".substrate/acceptance/deferrals.yaml";
5146
- const JourneyDeferralSchema = z.object({
5147
- journey: z.string().min(1),
5148
- reason: z.string().min(1, "a deferral must carry an operator reason"),
5149
- deferred_at: z.string().optional()
5150
- });
5151
- const JourneyDeferralsFileSchema = z.object({ deferrals: z.array(JourneyDeferralSchema) });
5152
- /** Parse deferrals YAML. Never throws; malformed content comes back as issues. */
5153
- function parseJourneyDeferrals(yamlContent) {
5154
- let doc;
5155
- try {
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
- if (doc === null || doc === void 0) return {
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
- * Extract + validate the `acceptance:` block from project-profile YAML
5205
- * content. `absent` when the profile has no block (legal — the coverage
5206
- * audit escalates registry-present+contract-absent as acceptance-unrunnable
5207
- * in blocking mode; pure accounting still works without one).
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 parseAcceptanceContract(profileYamlContent) {
5210
- let doc;
5211
- try {
5212
- doc = load(profileYamlContent);
5213
- } catch (err) {
5214
- const message = err instanceof YAMLException ? err.message : String(err);
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
- status: "invalid",
5217
- issues: [{
5218
- path: "(root)",
5219
- message: `malformed profile YAML: ${message}`
5220
- }]
5286
+ storyKey,
5287
+ matrix: matrix$1,
5288
+ confidence: "approximate",
5289
+ warnings
5221
5290
  };
5222
5291
  }
5223
- if (doc === null || doc === void 0 || typeof doc !== "object") return { status: "absent" };
5224
- const block = doc["acceptance"];
5225
- if (block === void 0 || block === null) return { status: "absent" };
5226
- const result = AcceptanceContractSchema.safeParse(block);
5227
- if (!result.success) return {
5228
- status: "invalid",
5229
- issues: result.error.issues.map((issue) => ({
5230
- path: issue.path.length > 0 ? `acceptance.${issue.path.map(String).join(".")}` : "acceptance",
5231
- message: issue.message
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
- status: "ok",
5236
- contract: result.data
5320
+ storyKey,
5321
+ matrix,
5322
+ confidence: "approximate",
5323
+ warnings
5237
5324
  };
5238
5325
  }
5239
5326
 
@@ -6823,4 +6910,4 @@ async function resolveRunManifest(dbRoot, runId) {
6823
6910
 
6824
6911
  //#endregion
6825
6912
  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-DZDlSq_s.js.map
6913
+ //# sourceMappingURL=manifest-read-C0N7b9nH.js.map
@@ -1,6 +1,6 @@
1
1
  import "../../logger-KeHncl-f.js";
2
2
  import "../../dist-CpnLJuVw.js";
3
- import "../../manifest-read-DZDlSq_s.js";
4
- import { runInteractivePrompt } from "../../interactive-prompt-C4Is1QDB.js";
3
+ import "../../manifest-read-C0N7b9nH.js";
4
+ import { runInteractivePrompt } from "../../interactive-prompt-BvY2FKsl.js";
5
5
 
6
6
  export { runInteractivePrompt };
@@ -1,11 +1,11 @@
1
- import { BMAD_BASELINE_TOKENS_FULL, FileKvStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, validateStoryKey } from "./health-BUl3y64N.js";
1
+ import { BMAD_BASELINE_TOKENS_FULL, FileKvStore, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN, VALID_PHASES, __commonJS, __require, __toESM, buildPipelineStatusOutput, createDatabaseAdapter, formatOutput, formatPipelineSummary, formatTokenTelemetry, inspectProcessTree, parseDbTimestampAsUtc, validateStoryKey } from "./health-hYMxKqAH.js";
2
2
  import { createLogger } from "./logger-KeHncl-f.js";
3
3
  import { TypedEventBusImpl, createEventBus, createTuiApp, isTuiCapable, printNonTtyWarning, sleep } from "./helpers-CElYrONe.js";
4
4
  import { ADVISORY_NOTES, BRANCH_PREFIX, CLAUDE_AUTH_FAILURE_HINT, CODEX_SANDBOX_BLOCK_HINT, Categorizer, ConsumerAnalyzer, DEFAULT_GLOBAL_SETTINGS, DispatcherImpl, DoltClient, ESCALATION_DIAGNOSIS, EXPERIMENT_RESULT, EfficiencyScorer, IngestionServer, LogTurnAnalyzer, OPERATIONAL_FINDING, Recommender, RoutingRecommender, RoutingResolver, RoutingTelemetry, RoutingTokenAccumulator, RoutingTuner, STORY_METRICS, STORY_OUTCOME, SubstrateConfigSchema, TEST_EXPANSION_FINDING, TEST_PLAN, TelemetryNormalizer, TelemetryPipeline, TurnAnalyzer, addTokenUsage, aggregateTokenUsageForRun, aggregateTokenUsageForStory, callLLM, classifyVersionGap, createConfigSystem, createDatabaseAdapter$1, createDecision, createGitWorktreeManager, createPipelineRun, createRequirement, detectClaudeAuthFailure, detectCodexSandboxBlock, detectInterfaceChanges, getArtifactByTypeForRun, getArtifactsByRun, getDecisionsByCategory, getDecisionsByPhase, getDecisionsByPhaseForRun, getLatestRun, getPipelineRunById, getRunMetrics, getRunningPipelineRuns, getStoryMetricsForRun, getTokenUsageSummary, initSchema, listRequirements, loadModelRoutingConfig, registerArtifact, swallowDebug, updatePipelineRun, updatePipelineRunConfig, upsertDecision, writeRunMetrics, writeStoryMetrics } from "./dist-CpnLJuVw.js";
5
- import { ACCEPTANCE_CONTRACT_PROFILE_PATH, FindingsInjector, JOURNEY_DEFERRALS_PATH, RunManifest, RuntimeProbeListSchema, applyConfigToGraph, computeJourneyCoverage, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, loadAcceptanceContractFromTrustedTree, loadJourneyDeferralsFromTrustedTree, loadJourneyRegistryFromTrustedTree, parseRuntimeProbes, parseStoryFrontmatter, renderFindings, resolveGraphPath, resolveMainRepoRoot, runAcTraceabilityCheck, runStaleVerificationRecovery, summarizeCoverage } from "./manifest-read-DZDlSq_s.js";
5
+ import { ACCEPTANCE_CONTRACT_PROFILE_PATH, FindingsInjector, JOURNEY_DEFERRALS_PATH, RunManifest, RuntimeProbeListSchema, applyConfigToGraph, computeJourneyCoverage, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, loadAcceptanceContractFromTrustedTree, loadJourneyDeferralsFromTrustedTree, loadJourneyRegistryFromTrustedTree, parseRuntimeProbes, parseStoryFrontmatter, renderFindings, resolveGraphPath, resolveMainRepoRoot, runAcTraceabilityCheck, runStaleVerificationRecovery, summarizeCoverage } from "./manifest-read-C0N7b9nH.js";
6
6
  import { WorkGraphRepository, detectCycles } from "./work-graph-repository-DZyJv5pV.js";
7
7
  import { deriveExitCode, routeDecision } from "./decision-router-BAPpON_C.js";
8
- import { runInteractivePrompt } from "./interactive-prompt-C4Is1QDB.js";
8
+ import { runInteractivePrompt } from "./interactive-prompt-BvY2FKsl.js";
9
9
  import { runRecoveryEngine } from "./recovery-engine-BKGBeBnW.js";
10
10
  import { basename, dirname, extname, join } from "path";
11
11
  import { access, readFile, readdir, stat } from "fs/promises";
@@ -12202,6 +12202,28 @@ function assembleVerificationContext(opts) {
12202
12202
  const profileBuildCommand = readProfileKey(trustedDir, "buildCommand");
12203
12203
  const profileTestCommand = readProfileKey(trustedDir, "testCommand");
12204
12204
  const trustedLanguages = readProfileLanguagesLocal(trustedDir);
12205
+ let acceptanceSpecGuard;
12206
+ if (opts.trustedProfileDir !== void 0) {
12207
+ const readOrNull = (rel) => {
12208
+ try {
12209
+ const content = readFileSync(join$1(trustedDir, rel), "utf-8");
12210
+ return content.trim().length > 0 ? content : null;
12211
+ } catch {
12212
+ return null;
12213
+ }
12214
+ };
12215
+ const journeysTrusted = readOrNull(".substrate/acceptance/journeys.yaml");
12216
+ if (journeysTrusted !== null) {
12217
+ const profileTrusted = readOrNull(".substrate/project-profile.yaml");
12218
+ const fixturesPath = readAcceptanceFixturesPathLocal(profileTrusted);
12219
+ acceptanceSpecGuard = {
12220
+ journeysTrusted,
12221
+ deferralsTrusted: readOrNull(".substrate/acceptance/deferrals.yaml"),
12222
+ profileTrusted,
12223
+ ...fixturesPath !== void 0 ? { fixturesPath } : {}
12224
+ };
12225
+ }
12226
+ }
12205
12227
  return {
12206
12228
  storyKey: opts.storyKey,
12207
12229
  workingDir: opts.workingDir,
@@ -12217,10 +12239,32 @@ function assembleVerificationContext(opts) {
12217
12239
  ...opts.modifiedTrackedFiles !== void 0 ? { modifiedTrackedFiles: opts.modifiedTrackedFiles } : {},
12218
12240
  ...profileBuildCommand !== void 0 ? { buildCommand: profileBuildCommand } : {},
12219
12241
  ...profileTestCommand !== void 0 ? { testCommand: profileTestCommand } : {},
12220
- ...trustedLanguages.length > 0 ? { trustedLanguages } : {}
12242
+ ...trustedLanguages.length > 0 ? { trustedLanguages } : {},
12243
+ ...acceptanceSpecGuard !== void 0 ? { acceptanceSpecGuard } : {}
12221
12244
  };
12222
12245
  }
12223
12246
  /**
12247
+ * A1.3: extract `fixtures:` from the `acceptance:` block of profile content.
12248
+ * Line-based (no yaml dependency, no sdlc value import) — tracks entry/exit
12249
+ * of the top-level acceptance block by indentation.
12250
+ */
12251
+ function readAcceptanceFixturesPathLocal(profileContent) {
12252
+ if (profileContent === null) return void 0;
12253
+ let inAcceptance = false;
12254
+ for (const line of profileContent.split("\n")) {
12255
+ if (/^acceptance:\s*$/.test(line)) {
12256
+ inAcceptance = true;
12257
+ continue;
12258
+ }
12259
+ if (inAcceptance && /^\S/.test(line)) inAcceptance = false;
12260
+ if (inAcceptance) {
12261
+ const m = /^\s+fixtures:\s*['"]?([^'"\n#]+?)['"]?\s*$/.exec(line);
12262
+ if (m?.[1] !== void 0) return m[1].trim();
12263
+ }
12264
+ }
12265
+ return void 0;
12266
+ }
12267
+ /**
12224
12268
  * Read a single `<key>: value` line from `.substrate/project-profile.yaml`
12225
12269
  * under `workingDir` (H1.2). Line-based parse — no yaml dependency.
12226
12270
  */
@@ -48318,4 +48362,4 @@ function registerRunCommand(program, version = "0.0.0", projectRoot = process.cw
48318
48362
 
48319
48363
  //#endregion
48320
48364
  export { AdapterTelemetryPersistence, AppError, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, EpicIngester, GLOBSTAR$1 as GLOBSTAR, GitClient, GrammarLoader, Minimatch$1 as Minimatch, Minipass, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SymbolParser, createContextCompiler, createDispatcher, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStopAfterGate, createTelemetryAdvisor, escape$1 as escape, formatPhaseCompletionSummary, getFactoryRunSummaries, getScenarioResultsForRun, getTwinRunsForRun, listGraphRuns, normalizeGraphSummaryToStatus, registerExportCommand, registerFactoryCommand, registerRunCommand, registerScenariosCommand, resolveMaxReviewCycles, resolveProbeAuthorStateIntegrating, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runProbeAuthor, runRunAction, runSolutioningPhase, unescape$1 as unescape, validateStopAfterFromConflict, wireNdjsonEmitter };
48321
- //# sourceMappingURL=run-nSb1TD3o.js.map
48365
+ //# sourceMappingURL=run-C7PLGWT0.js.map
@@ -1,14 +1,14 @@
1
- import "./health-BUl3y64N.js";
1
+ import "./health-hYMxKqAH.js";
2
2
  import "./logger-KeHncl-f.js";
3
3
  import "./helpers-CElYrONe.js";
4
4
  import "./dist-CpnLJuVw.js";
5
- import "./manifest-read-DZDlSq_s.js";
6
- import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, resolveProbeAuthorStateIntegrating, runRunAction, wireNdjsonEmitter } from "./run-nSb1TD3o.js";
5
+ import "./manifest-read-C0N7b9nH.js";
6
+ import { normalizeGraphSummaryToStatus, registerRunCommand, resolveMaxReviewCycles, resolveProbeAuthorStateIntegrating, runRunAction, wireNdjsonEmitter } from "./run-C7PLGWT0.js";
7
7
  import "./routing-DFxoKHDt.js";
8
8
  import "./work-graph-repository-DZyJv5pV.js";
9
9
  import "./decisions-CzSIEeGP.js";
10
10
  import "./decision-router-BAPpON_C.js";
11
- import "./interactive-prompt-C4Is1QDB.js";
11
+ import "./interactive-prompt-BvY2FKsl.js";
12
12
  import "./recovery-engine-BKGBeBnW.js";
13
13
 
14
14
  export { runRunAction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "substrate-ai",
3
- "version": "0.21.4",
3
+ "version": "0.21.5",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",