substrate-ai 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7059,17 +7059,35 @@ var DispatcherImpl = class {
7059
7059
  /**
7060
7060
  * Detect the package manager / build system used in a project.
7061
7061
  *
7062
- * Checks for language-specific markers in priority order:
7063
- * 1. Node.js lockfiles corresponding `<pm> run build`
7064
- * 2. Python markers (pyproject.toml, poetry.lock, setup.py) skip (no universal build step)
7065
- * 3. Rust (Cargo.toml)cargo build
7066
- * 4. Go (go.mod) → go build ./...
7067
- * 5. No markers found → skip (empty command)
7062
+ * Checks for build system markers in priority order:
7063
+ * 0. `.substrate/project-profile.yaml``project.buildCommand` field (most explicit, wins)
7064
+ * 1. `turbo.json``turbo build`
7065
+ * 2. Node.js lockfiles corresponding `<pm> run build`
7066
+ * 3. Python markers (pyproject.toml, poetry.lock, setup.py) → skip (no universal build step)
7067
+ * 4. Rust (Cargo.toml) → skip
7068
+ * 5. Go (go.mod) → skip
7069
+ * 6. No markers found → skip (empty command)
7068
7070
  *
7069
7071
  * When a non-Node.js project is detected (or nothing is recognized), the
7070
7072
  * returned command is '' which causes runBuildVerification() to skip.
7071
7073
  */
7072
7074
  function detectPackageManager(projectRoot) {
7075
+ const profilePath = join$1(projectRoot, ".substrate", "project-profile.yaml");
7076
+ if (existsSync$1(profilePath)) try {
7077
+ const raw = readFileSync$1(profilePath, "utf-8");
7078
+ const parsed = yaml.load(raw);
7079
+ const buildCommand = parsed?.project?.buildCommand;
7080
+ if (typeof buildCommand === "string" && buildCommand.length > 0) return {
7081
+ packageManager: "none",
7082
+ lockfile: "project-profile.yaml",
7083
+ command: buildCommand
7084
+ };
7085
+ } catch {}
7086
+ if (existsSync$1(join$1(projectRoot, "turbo.json"))) return {
7087
+ packageManager: "none",
7088
+ lockfile: "turbo.json",
7089
+ command: "turbo build"
7090
+ };
7073
7091
  const nodeCandidates = [
7074
7092
  {
7075
7093
  file: "pnpm-lock.yaml",
@@ -8938,6 +8956,11 @@ async function getImplementationDecisions(deps) {
8938
8956
  *
8939
8957
  * Returns the matched section content (from heading to next story heading or end),
8940
8958
  * or null if no matching section is found (caller falls back to full shard).
8959
+ *
8960
+ * @deprecated Used only as a migration shim for pre-37-0 projects that have
8961
+ * per-epic (key=epicId) shards in the decision store. Post-37-0 shards are
8962
+ * keyed by storyKey directly and do not need extraction. Do not delete until
8963
+ * all per-epic shards have been superseded by per-story shards (AC6).
8941
8964
  */
8942
8965
  function extractStorySection(shardContent, storyKey) {
8943
8966
  if (!shardContent || !storyKey) return null;
@@ -8955,13 +8978,26 @@ function extractStorySection(shardContent, storyKey) {
8955
8978
  }
8956
8979
  /**
8957
8980
  * Retrieve the epic shard from the pre-fetched implementation decisions.
8958
- * Looks for decisions with category='epic-shard', key=epicId.
8959
- * Falls back to reading _bmad-output/epics.md on disk if decisions are empty.
8960
8981
  *
8961
- * When storyKey is provided, extracts only the section for that story (AC3).
8982
+ * Lookup order (post-37-0 schema):
8983
+ * 1. Direct per-story lookup: category='epic-shard', key=storyKey → AC4
8984
+ * If found, return content immediately — no extractStorySection() needed.
8985
+ * 2. Migration shim (pre-37-0 fallback): category='epic-shard', key=epicId
8986
+ * + extractStorySection() to narrow to the requested story. → AC6
8987
+ * 3. File-based fallback: read epics.md from disk + extractStorySection(). → AC6
8962
8988
  */
8963
8989
  function getEpicShard(decisions, epicId, projectRoot, storyKey) {
8964
8990
  try {
8991
+ if (storyKey) {
8992
+ const perStoryShard = decisions.find((d) => d.category === "epic-shard" && d.key === storyKey);
8993
+ if (perStoryShard?.value) {
8994
+ logger$20.debug({
8995
+ epicId,
8996
+ storyKey
8997
+ }, "Found per-story epic shard (direct lookup)");
8998
+ return perStoryShard.value;
8999
+ }
9000
+ }
8965
9001
  const epicShard = decisions.find((d) => d.category === "epic-shard" && d.key === epicId);
8966
9002
  const shardContent = epicShard?.value;
8967
9003
  if (shardContent) {
@@ -8971,7 +9007,7 @@ function getEpicShard(decisions, epicId, projectRoot, storyKey) {
8971
9007
  logger$20.debug({
8972
9008
  epicId,
8973
9009
  storyKey
8974
- }, "Extracted per-story section from epic shard");
9010
+ }, "Extracted per-story section from epic shard (pre-37-0 fallback)");
8975
9011
  return storySection;
8976
9012
  }
8977
9013
  logger$20.debug({
@@ -9525,12 +9561,9 @@ function detectDeprecatedStatusField(content) {
9525
9561
  }
9526
9562
 
9527
9563
  //#endregion
9528
- //#region src/modules/compiled-workflows/dev-story.ts
9529
- const logger$16 = createLogger("compiled-workflows:dev-story");
9530
- /** Default timeout for dev-story dispatches in milliseconds (30 min) */
9531
- const DEFAULT_TIMEOUT_MS$1 = 18e5;
9532
- /** Default Vitest test patterns injected when no test-pattern decisions exist */
9533
- const DEFAULT_VITEST_PATTERNS = `## Test Patterns (defaults)
9564
+ //#region src/modules/compiled-workflows/default-test-patterns.ts
9565
+ /** Default test patterns for Vitest/Jest/Mocha (Node.js ecosystem) */
9566
+ const VITEST_DEFAULT_PATTERNS = `## Test Patterns (defaults)
9534
9567
  - Framework: Vitest (NOT jest — --testPathPattern flag does not work, use -- "pattern")
9535
9568
  - Mock approach: vi.mock() with hoisting for module-level mocks
9536
9569
  - Assertion style: expect().toBe(), expect().toEqual(), expect().toThrow()
@@ -9540,6 +9573,105 @@ const DEFAULT_VITEST_PATTERNS = `## Test Patterns (defaults)
9540
9573
  npx vitest run --no-coverage -- "your-module-name"
9541
9574
  - Final validation ONLY: npm test 2>&1 | grep -E "Test Files|Tests " | tail -3
9542
9575
  - Do NOT run the full suite (npm test) repeatedly — it consumes excessive memory when multiple agents run in parallel`;
9576
+ /** Default test patterns for Go (stdlib testing) */
9577
+ const GO_DEFAULT_PATTERNS = `## Test Patterns (defaults)
9578
+ - Framework: Go test (stdlib)
9579
+ - Test file naming: <module>_test.go alongside source files
9580
+ - Test structure: table-driven tests using t.Run() subtests
9581
+ - Run all tests: go test ./...
9582
+ - Run specific test: go test ./... -v -run TestFunctionName
9583
+ - IMPORTANT: Run targeted tests during development: go test ./pkg/... -v -run TestSpecific
9584
+ - Assertion style: t.Errorf(), t.Fatalf(); use testify if already in go.mod (require.Equal, assert.NoError)`;
9585
+ /** Default test patterns for Gradle (JUnit 5) */
9586
+ const GRADLE_DEFAULT_PATTERNS = `## Test Patterns (defaults)
9587
+ - Framework: JUnit 5 (Gradle)
9588
+ - Test structure: @Test annotated methods in class under src/test/
9589
+ - Run all tests: ./gradlew test
9590
+ - Run specific test: ./gradlew test --tests "com.example.ClassName.methodName"
9591
+ - IMPORTANT: Run targeted tests during development: ./gradlew test --tests "ClassName"
9592
+ - Assertion style: assertThat(...).isEqualTo(...) (AssertJ) or assertEquals (JUnit)`;
9593
+ /** Default test patterns for Maven (JUnit 5) */
9594
+ const MAVEN_DEFAULT_PATTERNS = `## Test Patterns (defaults)
9595
+ - Framework: JUnit 5 (Maven)
9596
+ - Test structure: @Test annotated methods in class under src/test/
9597
+ - Run all tests: mvn test
9598
+ - Run specific test: mvn test -Dtest="ClassName#methodName"
9599
+ - IMPORTANT: Run targeted tests during development: mvn test -Dtest="ClassName"
9600
+ - Assertion style: assertThat(...).isEqualTo(...) (AssertJ) or assertEquals (JUnit)`;
9601
+ /** Default test patterns for Cargo (Rust) */
9602
+ const CARGO_DEFAULT_PATTERNS = `## Test Patterns (defaults)
9603
+ - Framework: Rust test (cargo)
9604
+ - Test file naming: #[cfg(test)] module in same file, or tests/ directory for integration tests
9605
+ - Test structure: #[test] annotated functions
9606
+ - Run all tests: cargo test
9607
+ - Run specific test: cargo test test_function_name
9608
+ - IMPORTANT: Run targeted tests during development: cargo test --lib test_module
9609
+ - Assertion style: assert_eq!, assert!, assert_ne! macros`;
9610
+ /** Default test patterns for pytest (Python) */
9611
+ const PYTEST_DEFAULT_PATTERNS = `## Test Patterns (defaults)
9612
+ - Framework: pytest
9613
+ - Test file naming: test_<module>.py or <module>_test.py
9614
+ - Test structure: test_* functions or Test* classes with test_* methods
9615
+ - Run all tests: pytest
9616
+ - Run specific test: pytest tests/test_foo.py::test_bar -v
9617
+ - IMPORTANT: Run targeted tests during development: pytest -k "test_name" -v
9618
+ - Assertion style: plain assert statements; use pytest.raises() for exceptions`;
9619
+ /**
9620
+ * Resolve the appropriate default test pattern block for the project.
9621
+ *
9622
+ * Algorithm:
9623
+ * 1. If projectRoot is undefined or empty → return VITEST_DEFAULT_PATTERNS
9624
+ * 2. Build profile path: join(projectRoot, '.substrate/project-profile.yaml')
9625
+ * 3. If file does not exist → return VITEST_DEFAULT_PATTERNS
9626
+ * 4. Parse YAML; on error → return VITEST_DEFAULT_PATTERNS
9627
+ * 5. Match project.testCommand (case-insensitive substring):
9628
+ * go test → GO, gradlew/gradle → GRADLE, mvn → MAVEN,
9629
+ * cargo test → CARGO, pytest → PYTEST, vitest/jest/mocha/npm → VITEST
9630
+ * 6. If testCommand unmatched, try project.language:
9631
+ * go → GO, kotlin/java → GRADLE, rust → CARGO, python → PYTEST,
9632
+ * typescript/javascript → VITEST
9633
+ * 7. Nothing matched → return VITEST_DEFAULT_PATTERNS
9634
+ *
9635
+ * @param projectRoot - Absolute path to the project root (or undefined)
9636
+ * @returns Stack-appropriate test pattern block string
9637
+ */
9638
+ function resolveDefaultTestPatterns(projectRoot) {
9639
+ if (!projectRoot) return VITEST_DEFAULT_PATTERNS;
9640
+ const profilePath = join$1(projectRoot, ".substrate/project-profile.yaml");
9641
+ if (!existsSync$1(profilePath)) return VITEST_DEFAULT_PATTERNS;
9642
+ let profile = null;
9643
+ try {
9644
+ const content = readFileSync$1(profilePath, "utf-8");
9645
+ profile = yaml.load(content);
9646
+ } catch {
9647
+ return VITEST_DEFAULT_PATTERNS;
9648
+ }
9649
+ if (!profile) return VITEST_DEFAULT_PATTERNS;
9650
+ const project = profile["project"];
9651
+ if (!project) return VITEST_DEFAULT_PATTERNS;
9652
+ const testCommand = (project["testCommand"] ?? "").toLowerCase();
9653
+ if (testCommand) {
9654
+ if (testCommand.includes("cargo test")) return CARGO_DEFAULT_PATTERNS;
9655
+ if (testCommand.includes("go test")) return GO_DEFAULT_PATTERNS;
9656
+ if (testCommand.includes("gradlew") || testCommand.includes("gradle")) return GRADLE_DEFAULT_PATTERNS;
9657
+ if (testCommand.includes("mvn")) return MAVEN_DEFAULT_PATTERNS;
9658
+ if (testCommand.includes("pytest")) return PYTEST_DEFAULT_PATTERNS;
9659
+ if (testCommand.includes("vitest") || testCommand.includes("jest") || testCommand.includes("mocha") || testCommand.includes("npm")) return VITEST_DEFAULT_PATTERNS;
9660
+ }
9661
+ const language = (project["language"] ?? "").toLowerCase();
9662
+ if (language === "go") return GO_DEFAULT_PATTERNS;
9663
+ if (language === "kotlin" || language === "java") return GRADLE_DEFAULT_PATTERNS;
9664
+ if (language === "rust") return CARGO_DEFAULT_PATTERNS;
9665
+ if (language === "python") return PYTEST_DEFAULT_PATTERNS;
9666
+ if (language === "typescript" || language === "javascript") return VITEST_DEFAULT_PATTERNS;
9667
+ return VITEST_DEFAULT_PATTERNS;
9668
+ }
9669
+
9670
+ //#endregion
9671
+ //#region src/modules/compiled-workflows/dev-story.ts
9672
+ const logger$16 = createLogger("compiled-workflows:dev-story");
9673
+ /** Default timeout for dev-story dispatches in milliseconds (30 min) */
9674
+ const DEFAULT_TIMEOUT_MS$1 = 18e5;
9543
9675
  /**
9544
9676
  * Execute the compiled dev-story workflow.
9545
9677
  *
@@ -9654,8 +9786,8 @@ async function runDevStory(deps, params) {
9654
9786
  count: testPatternDecisions.length
9655
9787
  }, "Loaded test patterns from decision store");
9656
9788
  } else {
9657
- testPatternsContent = DEFAULT_VITEST_PATTERNS;
9658
- logger$16.debug({ storyKey }, "No test-pattern decisions found — using default Vitest patterns");
9789
+ testPatternsContent = resolveDefaultTestPatterns(deps.projectRoot);
9790
+ logger$16.debug({ storyKey }, "No test-pattern decisions — using stack-aware defaults");
9659
9791
  }
9660
9792
  } catch (err) {
9661
9793
  const error = err instanceof Error ? err.message : String(err);
@@ -9663,7 +9795,7 @@ async function runDevStory(deps, params) {
9663
9795
  storyKey,
9664
9796
  error
9665
9797
  }, "Failed to load test patterns — using defaults");
9666
- testPatternsContent = DEFAULT_VITEST_PATTERNS;
9798
+ testPatternsContent = resolveDefaultTestPatterns(deps.projectRoot);
9667
9799
  }
9668
9800
  const taskScopeContent = taskScope !== void 0 && taskScope.trim().length > 0 ? `## Task Scope for This Batch\n\nImplement ONLY the following tasks from the story:\n\n${taskScope}\n\nDo NOT implement tasks outside this list. Other tasks will be handled in separate batch dispatches.` : "";
9669
9801
  const priorFilesContent = priorFiles !== void 0 && priorFiles.length > 0 ? `## Files Modified by Previous Batches\n\nThe following files were created or modified by prior batch dispatches. Review them for context before implementing:\n\n${priorFiles.map((f) => `- ${f}`).join("\n")}` : "";
@@ -10362,15 +10494,40 @@ async function runTestPlan(deps, params) {
10362
10494
  return makeTestPlanFailureResult(`story_file_read_error: ${error}`);
10363
10495
  }
10364
10496
  const archConstraintsContent = await getArchConstraints$1(deps);
10365
- const { prompt, tokenCount, truncated } = assemblePrompt(template, [{
10366
- name: "story_content",
10367
- content: storyContent,
10368
- priority: "required"
10369
- }, {
10370
- name: "architecture_constraints",
10371
- content: archConstraintsContent,
10372
- priority: "optional"
10373
- }], TOKEN_CEILING);
10497
+ let testPatternsContent = "";
10498
+ try {
10499
+ const solutioningDecisions = await getDecisionsByPhase(deps.db, "solutioning");
10500
+ const testPatternDecisions = solutioningDecisions.filter((d) => d.category === "test-patterns");
10501
+ if (testPatternDecisions.length > 0) {
10502
+ testPatternsContent = "## Test Patterns\n" + testPatternDecisions.map((d) => `- ${d.key}: ${d.value}`).join("\n");
10503
+ logger$14.debug({
10504
+ storyKey,
10505
+ count: testPatternDecisions.length
10506
+ }, "Loaded test patterns from decision store");
10507
+ } else {
10508
+ testPatternsContent = resolveDefaultTestPatterns(deps.projectRoot);
10509
+ logger$14.debug({ storyKey }, "No test-pattern decisions — using stack-aware defaults");
10510
+ }
10511
+ } catch {
10512
+ testPatternsContent = resolveDefaultTestPatterns(deps.projectRoot);
10513
+ }
10514
+ const { prompt, tokenCount, truncated } = assemblePrompt(template, [
10515
+ {
10516
+ name: "story_content",
10517
+ content: storyContent,
10518
+ priority: "required"
10519
+ },
10520
+ {
10521
+ name: "architecture_constraints",
10522
+ content: archConstraintsContent,
10523
+ priority: "optional"
10524
+ },
10525
+ {
10526
+ name: "test_patterns",
10527
+ content: testPatternsContent,
10528
+ priority: "optional"
10529
+ }
10530
+ ], TOKEN_CEILING);
10374
10531
  logger$14.info({
10375
10532
  storyKey,
10376
10533
  tokenCount,
@@ -10575,6 +10732,23 @@ async function runTestExpansion(deps, params) {
10575
10732
  });
10576
10733
  }
10577
10734
  const archConstraintsContent = await getArchConstraints(deps);
10735
+ let testPatternsContent = "";
10736
+ try {
10737
+ const solutioningDecisions = await getDecisionsByPhase(deps.db, "solutioning");
10738
+ const testPatternDecisions = solutioningDecisions.filter((d) => d.category === "test-patterns");
10739
+ if (testPatternDecisions.length > 0) {
10740
+ testPatternsContent = "## Test Patterns\n" + testPatternDecisions.map((d) => `- ${d.key}: ${d.value}`).join("\n");
10741
+ logger$13.debug({
10742
+ storyKey,
10743
+ count: testPatternDecisions.length
10744
+ }, "Loaded test patterns from decision store");
10745
+ } else {
10746
+ testPatternsContent = resolveDefaultTestPatterns(deps.projectRoot);
10747
+ logger$13.debug({ storyKey }, "No test-pattern decisions — using stack-aware defaults");
10748
+ }
10749
+ } catch {
10750
+ testPatternsContent = resolveDefaultTestPatterns(deps.projectRoot);
10751
+ }
10578
10752
  let gitDiffContent = "";
10579
10753
  if (filesModified && filesModified.length > 0) try {
10580
10754
  const templateTokens = countTokens(template);
@@ -10611,6 +10785,11 @@ async function runTestExpansion(deps, params) {
10611
10785
  content: gitDiffContent,
10612
10786
  priority: "important"
10613
10787
  },
10788
+ {
10789
+ name: "test_patterns",
10790
+ content: testPatternsContent,
10791
+ priority: "optional"
10792
+ },
10614
10793
  {
10615
10794
  name: "arch_constraints",
10616
10795
  content: archConstraintsContent,
@@ -11517,7 +11696,7 @@ function registerHealthCommand(program, _version = "0.0.0", projectRoot = proces
11517
11696
  const logger$11 = createLogger("implementation-orchestrator:seed");
11518
11697
  /** Max chars for the architecture summary seeded into decisions */
11519
11698
  const MAX_ARCH_CHARS = 6e3;
11520
- /** Max chars per epic shard (fallback when per-story extraction returns null) */
11699
+ /** Max chars per epic-shard decision value (per-story or per-epic fallback) */
11521
11700
  const MAX_EPIC_SHARD_CHARS = 12e3;
11522
11701
  /** Max chars for test patterns */
11523
11702
  const MAX_TEST_PATTERNS_CHARS = 2e3;
@@ -11634,15 +11813,18 @@ async function seedEpicShards(db, projectRoot) {
11634
11813
  const shards = parseEpicShards(content);
11635
11814
  let count = 0;
11636
11815
  for (const shard of shards) {
11637
- await createDecision(db, {
11638
- pipeline_run_id: null,
11639
- phase: "implementation",
11640
- category: "epic-shard",
11641
- key: shard.epicId,
11642
- value: shard.content.slice(0, MAX_EPIC_SHARD_CHARS),
11643
- rationale: "Seeded from planning artifacts at orchestrator startup"
11644
- });
11645
- count++;
11816
+ const subsections = parseStorySubsections(shard.epicId, shard.content);
11817
+ for (const subsection of subsections) {
11818
+ await createDecision(db, {
11819
+ pipeline_run_id: null,
11820
+ phase: "implementation",
11821
+ category: "epic-shard",
11822
+ key: subsection.key,
11823
+ value: subsection.content.slice(0, MAX_EPIC_SHARD_CHARS),
11824
+ rationale: "Seeded from planning artifacts at orchestrator startup"
11825
+ });
11826
+ count++;
11827
+ }
11646
11828
  }
11647
11829
  await db.exec("DELETE FROM decisions WHERE phase = 'implementation' AND category = 'epic-shard-hash' AND key = 'epics-file'");
11648
11830
  await createDecision(db, {
@@ -11751,13 +11933,99 @@ function parseEpicShards(content) {
11751
11933
  return shards;
11752
11934
  }
11753
11935
  /**
11936
+ * Parse an epic section's content into per-story subsections.
11937
+ *
11938
+ * Matches story headings using three patterns:
11939
+ * - Markdown headings: #{2,6} Story \d+-\d+ (e.g., ### Story 37-1: Title)
11940
+ * - Bold: **Story \d+-\d+** (e.g., **Story 37-1**)
11941
+ * - Bare key: \d+-\d+:\s (e.g., 37-1: Title — must start at line start)
11942
+ *
11943
+ * Each subsection spans from its heading to the next matching heading or EOF.
11944
+ *
11945
+ * AC3: If no story headings are found, returns a single per-epic fallback entry
11946
+ * keyed by epicId — preserving backward-compatible behaviour for unstructured epics.
11947
+ */
11948
+ function parseStorySubsections(epicId, epicContent) {
11949
+ const storyPattern = /(?:^#{2,6}\s+Story\s+(\d+-\d+)|^\*\*Story\s+(\d+-\d+)\*\*|^(\d+-\d+):\s)/gim;
11950
+ const matches = [];
11951
+ let match$1;
11952
+ while ((match$1 = storyPattern.exec(epicContent)) !== null) {
11953
+ const storyKey = match$1[1] ?? match$1[2] ?? match$1[3];
11954
+ if (storyKey !== void 0) matches.push({
11955
+ storyKey,
11956
+ startIdx: match$1.index
11957
+ });
11958
+ }
11959
+ if (matches.length === 0) return [{
11960
+ key: epicId,
11961
+ content: epicContent
11962
+ }];
11963
+ const result = [];
11964
+ for (let i = 0; i < matches.length; i++) {
11965
+ const entry = matches[i];
11966
+ const nextEntry = matches[i + 1];
11967
+ const start = entry.startIdx;
11968
+ const end = nextEntry !== void 0 ? nextEntry.startIdx : epicContent.length;
11969
+ const sectionContent = epicContent.slice(start, end).trim();
11970
+ if (sectionContent.length > 0) result.push({
11971
+ key: entry.storyKey,
11972
+ content: sectionContent
11973
+ });
11974
+ }
11975
+ return result;
11976
+ }
11977
+ /**
11978
+ * Read the project profile YAML synchronously.
11979
+ * Returns null on missing file, parse error, or unexpected shape.
11980
+ * Does NOT import from src/modules/project-profile/ — inline parse only.
11981
+ *
11982
+ * @internal
11983
+ */
11984
+ function readProfileSync(projectRoot) {
11985
+ const profilePath = join$1(projectRoot, ".substrate", "project-profile.yaml");
11986
+ if (!existsSync$1(profilePath)) return null;
11987
+ try {
11988
+ const content = readFileSync$1(profilePath, "utf-8");
11989
+ const parsed = yaml.load(content);
11990
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
11991
+ return null;
11992
+ } catch {
11993
+ return null;
11994
+ }
11995
+ }
11996
+ /**
11754
11997
  * Detect test framework and patterns from project configuration.
11755
- * Reads package.json to determine vitest/jest/mocha and generates pattern docs.
11998
+ *
11999
+ * Detection priority:
12000
+ * 1. Profile present + packages[] non-empty → buildMonorepoTestPatterns(packages)
12001
+ * 2. Profile present + testCommand non-empty → mapTestCommandToPatterns(testCommand)
12002
+ * 3. No profile / profile fallthrough:
12003
+ * a. package.json present → existing vitest/jest/mocha detection
12004
+ * b. go.mod present → buildGoTestPatterns(projectRoot)
12005
+ * c. build.gradle.kts or build.gradle → buildGradleTestPatterns(projectRoot)
12006
+ * d. pom.xml → buildMavenTestPatterns()
12007
+ * e. Cargo.toml → buildCargoTestPatterns()
12008
+ * f. pyproject.toml OR conftest.py → buildPytestPatterns(projectRoot)
12009
+ * 4. Nothing matched → undefined
12010
+ *
12011
+ * @internal exported for direct unit testing
11756
12012
  */
11757
12013
  function detectTestPatterns(projectRoot) {
12014
+ const profile = readProfileSync(projectRoot);
12015
+ if (profile !== null) {
12016
+ const project = profile["project"];
12017
+ if (project !== void 0) {
12018
+ const packages = project["packages"];
12019
+ if (Array.isArray(packages) && packages.length > 0) return buildMonorepoTestPatterns(packages);
12020
+ const testCommand = project["testCommand"];
12021
+ if (typeof testCommand === "string" && testCommand.length > 0) {
12022
+ const mapped = mapTestCommandToPatterns(testCommand);
12023
+ if (mapped !== void 0) return mapped;
12024
+ }
12025
+ }
12026
+ }
11758
12027
  const pkgPath = join$1(projectRoot, "package.json");
11759
- if (!existsSync$1(pkgPath)) return void 0;
11760
- try {
12028
+ if (existsSync$1(pkgPath)) try {
11761
12029
  const pkg = JSON.parse(readFileSync$1(pkgPath, "utf-8"));
11762
12030
  const allDeps = {
11763
12031
  ...pkg.dependencies,
@@ -11777,9 +12045,17 @@ function detectTestPatterns(projectRoot) {
11777
12045
  if (firstTest.includes("@jest") || firstTest.includes("jest.mock")) return buildJestPatterns(testScript);
11778
12046
  }
11779
12047
  return void 0;
11780
- } catch {
11781
- return void 0;
11782
- }
12048
+ } catch {}
12049
+ if (existsSync$1(join$1(projectRoot, "go.mod"))) return buildGoTestPatterns(projectRoot);
12050
+ if (existsSync$1(join$1(projectRoot, "build.gradle.kts")) || existsSync$1(join$1(projectRoot, "build.gradle"))) return buildGradleTestPatterns(projectRoot);
12051
+ if (existsSync$1(join$1(projectRoot, "pom.xml"))) return buildMavenTestPatterns();
12052
+ if (existsSync$1(join$1(projectRoot, "Cargo.toml"))) return buildCargoTestPatterns();
12053
+ if (existsSync$1(join$1(projectRoot, "conftest.py"))) return buildPytestPatterns(projectRoot);
12054
+ if (existsSync$1(join$1(projectRoot, "pyproject.toml"))) try {
12055
+ const pyprojectContent = readFileSync$1(join$1(projectRoot, "pyproject.toml"), "utf-8");
12056
+ if (pyprojectContent.includes("[tool.pytest")) return buildPytestPatterns(projectRoot);
12057
+ } catch {}
12058
+ return void 0;
11783
12059
  }
11784
12060
  function buildVitestPatterns(testScript) {
11785
12061
  const runCmd = testScript || "npx vitest run";
@@ -11816,6 +12092,196 @@ function buildMochaPatterns() {
11816
12092
  ].join("\n");
11817
12093
  }
11818
12094
  /**
12095
+ * Build Go test patterns.
12096
+ * Optionally detects testify from go.mod if projectRoot is non-empty.
12097
+ *
12098
+ * @internal
12099
+ */
12100
+ function buildGoTestPatterns(projectRoot) {
12101
+ let hasTestify = false;
12102
+ if (projectRoot.length > 0) try {
12103
+ const goModPath = join$1(projectRoot, "go.mod");
12104
+ if (existsSync$1(goModPath)) {
12105
+ const content = readFileSync$1(goModPath, "utf-8");
12106
+ hasTestify = content.includes("github.com/stretchr/testify");
12107
+ }
12108
+ } catch {}
12109
+ return [
12110
+ "## Test Patterns",
12111
+ "- Framework: Go test (stdlib)",
12112
+ "- Test file naming: <module>_test.go alongside source files",
12113
+ "- Test structure: table-driven tests with t.Run() subtests",
12114
+ "- Run all tests: go test ./...",
12115
+ "- Run specific test: go test ./... -v -run TestFunctionName",
12116
+ "- Assertion style: t.Errorf(), t.Fatalf()",
12117
+ hasTestify ? "- testify available: use require.Equal(), assert.NoError(), etc." : ""
12118
+ ].filter(Boolean).join("\n");
12119
+ }
12120
+ /**
12121
+ * Build Gradle (JVM) test patterns.
12122
+ * Detects JUnit5 vs JUnit4 if projectRoot is non-empty.
12123
+ *
12124
+ * @internal
12125
+ */
12126
+ function buildGradleTestPatterns(projectRoot) {
12127
+ let hasJunit5 = false;
12128
+ if (projectRoot.length > 0) try {
12129
+ const ktsPath = join$1(projectRoot, "build.gradle.kts");
12130
+ const groovyPath = join$1(projectRoot, "build.gradle");
12131
+ const buildFilePath = existsSync$1(ktsPath) ? ktsPath : groovyPath;
12132
+ if (existsSync$1(buildFilePath)) {
12133
+ const content = readFileSync$1(buildFilePath, "utf-8");
12134
+ hasJunit5 = content.includes("junit-jupiter");
12135
+ }
12136
+ } catch {}
12137
+ return [
12138
+ "## Test Patterns",
12139
+ `- Framework: ${hasJunit5 ? "JUnit 5" : "JUnit"}`,
12140
+ "- Run all tests: ./gradlew test",
12141
+ "- Run specific test: ./gradlew test --tests \"com.example.ClassName\"",
12142
+ "- Test annotation: @Test",
12143
+ hasJunit5 ? "- Assertion style: assertThat() (AssertJ), assertEquals()" : "- Assertion style: assertEquals(), assertThat()"
12144
+ ].join("\n");
12145
+ }
12146
+ /**
12147
+ * Build Maven (JVM) test patterns.
12148
+ *
12149
+ * @internal
12150
+ */
12151
+ function buildMavenTestPatterns() {
12152
+ return [
12153
+ "## Test Patterns",
12154
+ "- Framework: JUnit (Maven)",
12155
+ "- Run all tests: mvn test",
12156
+ "- Run specific test: mvn test -Dtest=ClassName",
12157
+ "- Test annotation: @Test",
12158
+ "- Assertion style: assertEquals(), assertThat()"
12159
+ ].join("\n");
12160
+ }
12161
+ /**
12162
+ * Build Cargo/Rust test patterns.
12163
+ *
12164
+ * @internal
12165
+ */
12166
+ function buildCargoTestPatterns() {
12167
+ return [
12168
+ "## Test Patterns",
12169
+ "- Framework: Rust built-in test harness (cargo test)",
12170
+ "- Run all tests: cargo test",
12171
+ "- Run specific test: cargo test module_name",
12172
+ "- Test annotation: #[test]",
12173
+ "- Assertion macros: assert_eq!(), assert!(), assert_ne!()",
12174
+ "- Test module structure: #[cfg(test)] mod tests { ... }"
12175
+ ].join("\n");
12176
+ }
12177
+ /**
12178
+ * Build pytest (Python) test patterns.
12179
+ * Checks for conftest.py and pyproject.toml for context.
12180
+ *
12181
+ * @internal
12182
+ */
12183
+ function buildPytestPatterns(projectRoot) {
12184
+ let hasConftest = false;
12185
+ if (projectRoot.length > 0) try {
12186
+ hasConftest = existsSync$1(join$1(projectRoot, "conftest.py"));
12187
+ } catch {}
12188
+ return [
12189
+ "## Test Patterns",
12190
+ "- Framework: pytest",
12191
+ "- Run all tests: pytest",
12192
+ "- Run specific test: pytest tests/test_file.py -v -k \"test_name\"",
12193
+ "- Fixture pattern: @pytest.fixture (define in conftest.py for sharing)",
12194
+ "- Assertion style: assert statement (plain Python)",
12195
+ hasConftest ? "- conftest.py detected: shared fixtures available" : ""
12196
+ ].filter(Boolean).join("\n");
12197
+ }
12198
+ /**
12199
+ * Map a profile testCommand string to appropriate pattern builder output.
12200
+ * Returns undefined for unrecognized commands.
12201
+ *
12202
+ * @internal
12203
+ */
12204
+ function mapTestCommandToPatterns(testCommand) {
12205
+ if (testCommand.includes("go test")) return buildGoTestPatterns("");
12206
+ if (testCommand.includes("gradlew") || testCommand.includes("gradle")) return buildGradleTestPatterns("");
12207
+ if (testCommand.includes("mvn")) return buildMavenTestPatterns();
12208
+ if (testCommand.includes("cargo test")) return buildCargoTestPatterns();
12209
+ if (testCommand.includes("pytest")) return buildPytestPatterns("");
12210
+ if (testCommand.includes("vitest")) return buildVitestPatterns(testCommand);
12211
+ if (testCommand.includes("jest")) return buildJestPatterns(testCommand);
12212
+ if (testCommand.includes("mocha")) return buildMochaPatterns();
12213
+ return void 0;
12214
+ }
12215
+ /**
12216
+ * Build combined test patterns for a monorepo with multiple language packages.
12217
+ * Emits a concise per-language block for each distinct language, prefixed with package path.
12218
+ *
12219
+ * @internal
12220
+ */
12221
+ function buildMonorepoTestPatterns(packages) {
12222
+ const seen = new Set();
12223
+ const entries = [];
12224
+ for (const pkg of packages) if (typeof pkg.language === "string" && pkg.language.length > 0 && !seen.has(pkg.language)) {
12225
+ seen.add(pkg.language);
12226
+ entries.push({
12227
+ language: pkg.language,
12228
+ path: pkg.path ?? ""
12229
+ });
12230
+ }
12231
+ const blocks = [];
12232
+ for (const entry of entries) {
12233
+ const header = entry.path.length > 0 ? `# ${entry.path} (${entry.language})` : `# ${entry.language}`;
12234
+ let block;
12235
+ switch (entry.language) {
12236
+ case "go":
12237
+ block = [
12238
+ header,
12239
+ "- go test ./...",
12240
+ "- go test ./... -v -run TestName",
12241
+ "- File naming: _test.go"
12242
+ ].join("\n");
12243
+ break;
12244
+ case "typescript":
12245
+ case "javascript":
12246
+ block = [
12247
+ header,
12248
+ "- npx vitest run (or npm test)",
12249
+ "- vi.mock() for mocking",
12250
+ "- describe/it structure"
12251
+ ].join("\n");
12252
+ break;
12253
+ case "java":
12254
+ case "kotlin":
12255
+ block = [
12256
+ header,
12257
+ "- ./gradlew test",
12258
+ "- @Test annotation",
12259
+ "- assertEquals() / assertThat()"
12260
+ ].join("\n");
12261
+ break;
12262
+ case "rust":
12263
+ block = [
12264
+ header,
12265
+ "- cargo test",
12266
+ "- #[test] attribute",
12267
+ "- assert_eq!() / assert!()"
12268
+ ].join("\n");
12269
+ break;
12270
+ case "python":
12271
+ block = [
12272
+ header,
12273
+ "- pytest",
12274
+ "- @pytest.fixture",
12275
+ "- assert statement style"
12276
+ ].join("\n");
12277
+ break;
12278
+ default: block = [header, `- Run tests for ${entry.language} package`].join("\n");
12279
+ }
12280
+ blocks.push(block);
12281
+ }
12282
+ return ["## Test Patterns", ...blocks].join("\n\n");
12283
+ }
12284
+ /**
11819
12285
  * Find a few test files in the project to help detect the test framework.
11820
12286
  */
11821
12287
  function findTestFiles(projectRoot) {
@@ -11992,6 +12458,47 @@ function parseInterfaceContracts(storyContent, storyKey) {
11992
12458
  //#endregion
11993
12459
  //#region src/modules/implementation-orchestrator/contract-verifier.ts
11994
12460
  /**
12461
+ * Reads .substrate/project-profile.yaml (Story 37-1) and determines whether
12462
+ * TypeScript type-checking is appropriate for this project.
12463
+ *
12464
+ * Detection order:
12465
+ * 1. No profile → true (preserve pre-37-4 behavior)
12466
+ * 2. `packages` array non-empty → true iff any package is typescript/javascript
12467
+ * 3. `packages` empty/absent → infer from `buildCommand` — true for npm/pnpm/yarn/bun/turbo/tsc
12468
+ * 4. Parse error → true (conservative, allow tsc)
12469
+ *
12470
+ * Uses synchronous I/O to avoid making verifyContracts async (Story 37-3 pattern).
12471
+ * Does NOT import from src/modules/project-profile/ to avoid circular-dependency risk.
12472
+ */
12473
+ function shouldRunTscCheck(projectRoot) {
12474
+ const profilePath = join$1(projectRoot, ".substrate", "project-profile.yaml");
12475
+ if (!existsSync$1(profilePath)) return true;
12476
+ try {
12477
+ const raw = readFileSync$1(profilePath, "utf-8");
12478
+ const parsed = yaml.load(raw);
12479
+ if (!parsed) return true;
12480
+ const project = parsed?.project;
12481
+ if (!project) return true;
12482
+ const packages = project["packages"];
12483
+ if (Array.isArray(packages) && packages.length > 0) return packages.some((p) => p.language === "typescript" || p.language === "javascript");
12484
+ const buildCommand = project["buildCommand"];
12485
+ if (typeof buildCommand === "string" && buildCommand.length > 0) {
12486
+ const tsIndicators = [
12487
+ "npm",
12488
+ "pnpm",
12489
+ "yarn",
12490
+ "bun",
12491
+ "tsc",
12492
+ "turbo"
12493
+ ];
12494
+ return tsIndicators.some((ind) => buildCommand.includes(ind));
12495
+ }
12496
+ return true;
12497
+ } catch {
12498
+ return true;
12499
+ }
12500
+ }
12501
+ /**
11995
12502
  * Verify all declared contract export/import pairs after sprint completion.
11996
12503
  *
11997
12504
  * @param declarations - All ContractDeclaration entries from the decision store
@@ -12023,80 +12530,82 @@ function verifyContracts(declarations, projectRoot) {
12023
12530
  });
12024
12531
  }
12025
12532
  }
12026
- const tsconfigPath = join$1(projectRoot, "tsconfig.json");
12027
- const tscBinPath = join$1(projectRoot, "node_modules", ".bin", "tsc");
12028
- if (existsSync$1(tsconfigPath) && existsSync$1(tscBinPath)) {
12029
- let tscOutput = "";
12030
- let tscFailed = false;
12031
- try {
12032
- execSync(`"${tscBinPath}" --noEmit`, {
12033
- cwd: projectRoot,
12034
- timeout: 12e4,
12035
- encoding: "utf-8",
12036
- stdio: [
12037
- "pipe",
12038
- "pipe",
12039
- "pipe"
12040
- ]
12041
- });
12042
- } catch (err) {
12043
- tscFailed = true;
12044
- if (err != null && typeof err === "object") {
12045
- const e = err;
12046
- const stdoutStr = typeof e.stdout === "string" ? e.stdout : Buffer.isBuffer(e.stdout) ? e.stdout.toString("utf-8") : "";
12047
- const stderrStr = typeof e.stderr === "string" ? e.stderr : Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf-8") : "";
12048
- tscOutput = [stdoutStr, stderrStr].filter((s) => s.length > 0).join("\n");
12049
- if (!tscOutput && e.message) tscOutput = e.message;
12050
- }
12051
- }
12052
- if (tscFailed) {
12053
- const truncatedOutput = tscOutput.slice(0, 1e3);
12054
- const matchedExports = new Set();
12055
- for (const exp of exports) {
12056
- if (!exp.filePath) continue;
12057
- if (tscOutput.includes(exp.filePath)) {
12058
- matchedExports.add(exp.contractName);
12059
- const importers = imports.filter((i) => i.contractName === exp.contractName);
12060
- if (importers.length > 0) for (const imp of importers) mismatches.push({
12061
- exporter: exp.storyKey,
12062
- importer: imp.storyKey,
12063
- contractName: exp.contractName,
12064
- mismatchDescription: `TypeScript type-check failed for ${exp.filePath}: ${truncatedOutput}`
12065
- });
12066
- else mismatches.push({
12067
- exporter: exp.storyKey,
12068
- importer: null,
12069
- contractName: exp.contractName,
12070
- mismatchDescription: `TypeScript type-check failed for ${exp.filePath}: ${truncatedOutput}`
12071
- });
12533
+ if (shouldRunTscCheck(projectRoot)) {
12534
+ const tsconfigPath = join$1(projectRoot, "tsconfig.json");
12535
+ const tscBinPath = join$1(projectRoot, "node_modules", ".bin", "tsc");
12536
+ if (existsSync$1(tsconfigPath) && existsSync$1(tscBinPath)) {
12537
+ let tscOutput = "";
12538
+ let tscFailed = false;
12539
+ try {
12540
+ execSync(`"${tscBinPath}" --noEmit`, {
12541
+ cwd: projectRoot,
12542
+ timeout: 12e4,
12543
+ encoding: "utf-8",
12544
+ stdio: [
12545
+ "pipe",
12546
+ "pipe",
12547
+ "pipe"
12548
+ ]
12549
+ });
12550
+ } catch (err) {
12551
+ tscFailed = true;
12552
+ if (err != null && typeof err === "object") {
12553
+ const e = err;
12554
+ const stdoutStr = typeof e.stdout === "string" ? e.stdout : Buffer.isBuffer(e.stdout) ? e.stdout.toString("utf-8") : "";
12555
+ const stderrStr = typeof e.stderr === "string" ? e.stderr : Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf-8") : "";
12556
+ tscOutput = [stdoutStr, stderrStr].filter((s) => s.length > 0).join("\n");
12557
+ if (!tscOutput && e.message) tscOutput = e.message;
12072
12558
  }
12073
12559
  }
12074
- if (matchedExports.size === 0) {
12075
- const reportedPairs = new Set();
12560
+ if (tscFailed) {
12561
+ const truncatedOutput = tscOutput.slice(0, 1e3);
12562
+ const matchedExports = new Set();
12076
12563
  for (const exp of exports) {
12077
- const importers = imports.filter((i) => i.contractName === exp.contractName);
12078
- if (importers.length > 0) for (const imp of importers) {
12079
- const pairKey = `${exp.storyKey}:${imp.storyKey}:${exp.contractName}`;
12080
- if (!reportedPairs.has(pairKey)) {
12081
- reportedPairs.add(pairKey);
12082
- mismatches.push({
12083
- exporter: exp.storyKey,
12084
- importer: imp.storyKey,
12085
- contractName: exp.contractName,
12086
- mismatchDescription: `TypeScript type-check failed: ${truncatedOutput}`
12087
- });
12088
- }
12564
+ if (!exp.filePath) continue;
12565
+ if (tscOutput.includes(exp.filePath)) {
12566
+ matchedExports.add(exp.contractName);
12567
+ const importers = imports.filter((i) => i.contractName === exp.contractName);
12568
+ if (importers.length > 0) for (const imp of importers) mismatches.push({
12569
+ exporter: exp.storyKey,
12570
+ importer: imp.storyKey,
12571
+ contractName: exp.contractName,
12572
+ mismatchDescription: `TypeScript type-check failed for ${exp.filePath}: ${truncatedOutput}`
12573
+ });
12574
+ else mismatches.push({
12575
+ exporter: exp.storyKey,
12576
+ importer: null,
12577
+ contractName: exp.contractName,
12578
+ mismatchDescription: `TypeScript type-check failed for ${exp.filePath}: ${truncatedOutput}`
12579
+ });
12089
12580
  }
12090
- else {
12091
- const pairKey = `${exp.storyKey}:null:${exp.contractName}`;
12092
- if (!reportedPairs.has(pairKey)) {
12093
- reportedPairs.add(pairKey);
12094
- mismatches.push({
12095
- exporter: exp.storyKey,
12096
- importer: null,
12097
- contractName: exp.contractName,
12098
- mismatchDescription: `TypeScript type-check failed: ${truncatedOutput}`
12099
- });
12581
+ }
12582
+ if (matchedExports.size === 0) {
12583
+ const reportedPairs = new Set();
12584
+ for (const exp of exports) {
12585
+ const importers = imports.filter((i) => i.contractName === exp.contractName);
12586
+ if (importers.length > 0) for (const imp of importers) {
12587
+ const pairKey = `${exp.storyKey}:${imp.storyKey}:${exp.contractName}`;
12588
+ if (!reportedPairs.has(pairKey)) {
12589
+ reportedPairs.add(pairKey);
12590
+ mismatches.push({
12591
+ exporter: exp.storyKey,
12592
+ importer: imp.storyKey,
12593
+ contractName: exp.contractName,
12594
+ mismatchDescription: `TypeScript type-check failed: ${truncatedOutput}`
12595
+ });
12596
+ }
12597
+ }
12598
+ else {
12599
+ const pairKey = `${exp.storyKey}:null:${exp.contractName}`;
12600
+ if (!reportedPairs.has(pairKey)) {
12601
+ reportedPairs.add(pairKey);
12602
+ mismatches.push({
12603
+ exporter: exp.storyKey,
12604
+ importer: null,
12605
+ contractName: exp.contractName,
12606
+ mismatchDescription: `TypeScript type-check failed: ${truncatedOutput}`
12607
+ });
12608
+ }
12100
12609
  }
12101
12610
  }
12102
12611
  }
@@ -22792,4 +23301,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
22792
23301
 
22793
23302
  //#endregion
22794
23303
  export { AdapterTelemetryPersistence, AppError, DEFAULT_CONFIG, DEFAULT_ROUTING_POLICY, DoltClient, DoltNotInstalled, DoltRepoMapMetaRepository, DoltSymbolRepository, ERR_REPO_MAP_STORAGE_WRITE, FileStateStore, GitClient, GrammarLoader, IngestionServer, RepoMapInjector, RepoMapModule, RepoMapQueryEngine, RepoMapStorage, SUBSTRATE_OWNED_SETTINGS_KEYS, SymbolParser, VALID_PHASES, WorkGraphRepository, buildPipelineStatusOutput, checkDoltInstalled, createConfigSystem, createContextCompiler, createDatabaseAdapter, createDispatcher, createDoltClient, createEventEmitter, createImplementationOrchestrator, createPackLoader, createPhaseOrchestrator, createStateStore, createStopAfterGate, createTelemetryAdvisor, detectCycles, findPackageRoot, formatOutput, formatPhaseCompletionSummary, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, initSchema, initializeDolt, isSyncAdapter, parseDbTimestampAsUtc, registerHealthCommand, registerRunCommand, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveMainRepoRoot, resolveStoryKeys, runAnalysisPhase, runPlanningPhase, runRunAction, runSolutioningPhase, validateStopAfterFromConflict };
22795
- //# sourceMappingURL=run-B1WEe6SY.js.map
23304
+ //# sourceMappingURL=run-IDOmPys1.js.map