substrate-ai 0.5.11 → 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
  }
@@ -12194,10 +12703,12 @@ const EfficiencyScoreSchema = z.object({
12194
12703
  cacheHitSubScore: z.number().min(0).max(100),
12195
12704
  ioRatioSubScore: z.number().min(0).max(100),
12196
12705
  contextManagementSubScore: z.number().min(0).max(100),
12706
+ tokenDensitySubScore: z.number().min(0).max(100).default(0),
12197
12707
  avgCacheHitRate: z.number(),
12198
12708
  avgIoRatio: z.number(),
12199
12709
  contextSpikeCount: z.number().int().nonnegative(),
12200
12710
  totalTurns: z.number().int().nonnegative(),
12711
+ coldStartTurnsExcluded: z.number().int().nonnegative().default(0),
12201
12712
  perModelBreakdown: z.array(ModelEfficiencySchema),
12202
12713
  perSourceBreakdown: z.array(SourceEfficiencySchema),
12203
12714
  dispatchId: z.string().optional(),
@@ -12297,6 +12808,8 @@ var AdapterTelemetryPersistence = class {
12297
12808
  total_turns INTEGER NOT NULL DEFAULT 0,
12298
12809
  per_model_json TEXT NOT NULL DEFAULT '[]',
12299
12810
  per_source_json TEXT NOT NULL DEFAULT '[]',
12811
+ token_density_sub_score DOUBLE NOT NULL DEFAULT 0,
12812
+ cold_start_turns_excluded INTEGER NOT NULL DEFAULT 0,
12300
12813
  dispatch_id TEXT,
12301
12814
  task_type TEXT,
12302
12815
  phase TEXT,
@@ -12307,6 +12820,12 @@ var AdapterTelemetryPersistence = class {
12307
12820
  CREATE INDEX IF NOT EXISTS idx_efficiency_story
12308
12821
  ON efficiency_scores (story_key, timestamp DESC)
12309
12822
  `);
12823
+ try {
12824
+ await this._adapter.exec(`ALTER TABLE efficiency_scores ADD COLUMN token_density_sub_score DOUBLE NOT NULL DEFAULT 0`);
12825
+ } catch {}
12826
+ try {
12827
+ await this._adapter.exec(`ALTER TABLE efficiency_scores ADD COLUMN cold_start_turns_excluded INTEGER NOT NULL DEFAULT 0`);
12828
+ } catch {}
12310
12829
  await this._adapter.exec(`
12311
12830
  CREATE TABLE IF NOT EXISTS recommendations (
12312
12831
  id VARCHAR(16) NOT NULL,
@@ -12443,13 +12962,17 @@ var AdapterTelemetryPersistence = class {
12443
12962
  await this._adapter.query(`INSERT INTO efficiency_scores (
12444
12963
  story_key, timestamp, composite_score,
12445
12964
  cache_hit_sub_score, io_ratio_sub_score, context_management_sub_score,
12965
+ token_density_sub_score,
12446
12966
  avg_cache_hit_rate, avg_io_ratio, context_spike_count, total_turns,
12967
+ cold_start_turns_excluded,
12447
12968
  per_model_json, per_source_json,
12448
12969
  dispatch_id, task_type, phase
12449
12970
  ) VALUES (
12450
12971
  ?, ?, ?,
12451
12972
  ?, ?, ?,
12973
+ ?,
12452
12974
  ?, ?, ?, ?,
12975
+ ?,
12453
12976
  ?, ?,
12454
12977
  ?, ?, ?
12455
12978
  )`, [
@@ -12459,10 +12982,12 @@ var AdapterTelemetryPersistence = class {
12459
12982
  score.cacheHitSubScore,
12460
12983
  score.ioRatioSubScore,
12461
12984
  score.contextManagementSubScore,
12985
+ score.tokenDensitySubScore ?? 0,
12462
12986
  score.avgCacheHitRate,
12463
12987
  score.avgIoRatio,
12464
12988
  score.contextSpikeCount,
12465
12989
  score.totalTurns,
12990
+ score.coldStartTurnsExcluded ?? 0,
12466
12991
  JSON.stringify(score.perModelBreakdown),
12467
12992
  JSON.stringify(score.perSourceBreakdown),
12468
12993
  score.dispatchId ?? null,
@@ -12482,10 +13007,12 @@ var AdapterTelemetryPersistence = class {
12482
13007
  cacheHitSubScore: row.cache_hit_sub_score,
12483
13008
  ioRatioSubScore: row.io_ratio_sub_score,
12484
13009
  contextManagementSubScore: row.context_management_sub_score,
13010
+ tokenDensitySubScore: row.token_density_sub_score ?? 0,
12485
13011
  avgCacheHitRate: row.avg_cache_hit_rate,
12486
13012
  avgIoRatio: row.avg_io_ratio,
12487
13013
  contextSpikeCount: row.context_spike_count,
12488
13014
  totalTurns: row.total_turns,
13015
+ coldStartTurnsExcluded: row.cold_start_turns_excluded ?? 0,
12489
13016
  perModelBreakdown: JSON.parse(row.per_model_json),
12490
13017
  perSourceBreakdown: JSON.parse(row.per_source_json),
12491
13018
  ...row.dispatch_id != null && { dispatchId: row.dispatch_id },
@@ -13119,8 +13646,53 @@ var IngestionServer = class {
13119
13646
  }
13120
13647
  };
13121
13648
 
13649
+ //#endregion
13650
+ //#region src/modules/telemetry/task-baselines.ts
13651
+ const TASK_BASELINES = {
13652
+ "dev-story": {
13653
+ expectedOutputPerTurn: 550,
13654
+ targetIoRatio: 100
13655
+ },
13656
+ "create-story": {
13657
+ expectedOutputPerTurn: 1500,
13658
+ targetIoRatio: 100
13659
+ },
13660
+ "code-review": {
13661
+ expectedOutputPerTurn: 3900,
13662
+ targetIoRatio: 50
13663
+ },
13664
+ "minor-fixes": {
13665
+ expectedOutputPerTurn: 700,
13666
+ targetIoRatio: 100
13667
+ },
13668
+ "test-plan": {
13669
+ expectedOutputPerTurn: 1600,
13670
+ targetIoRatio: 30
13671
+ },
13672
+ "test-expansion": {
13673
+ expectedOutputPerTurn: 1950,
13674
+ targetIoRatio: 15
13675
+ }
13676
+ };
13677
+ const DEFAULT_BASELINE = {
13678
+ expectedOutputPerTurn: 800,
13679
+ targetIoRatio: 100
13680
+ };
13681
+ /**
13682
+ * Get the baseline for a task type, falling back to DEFAULT_BASELINE
13683
+ * when taskType is undefined, empty, or unknown.
13684
+ */
13685
+ function getBaseline(taskType) {
13686
+ if (taskType === void 0 || taskType === "") return DEFAULT_BASELINE;
13687
+ return TASK_BASELINES[taskType] ?? DEFAULT_BASELINE;
13688
+ }
13689
+
13122
13690
  //#endregion
13123
13691
  //#region src/modules/telemetry/efficiency-scorer.ts
13692
+ const W_CACHE = .25;
13693
+ const W_IO_RATIO = .25;
13694
+ const W_CONTEXT = .25;
13695
+ const W_TOKEN_DENSITY = .25;
13124
13696
  var EfficiencyScorer = class {
13125
13697
  _logger;
13126
13698
  constructor(logger$27) {
@@ -13142,27 +13714,36 @@ var EfficiencyScorer = class {
13142
13714
  cacheHitSubScore: 0,
13143
13715
  ioRatioSubScore: 0,
13144
13716
  contextManagementSubScore: 0,
13717
+ tokenDensitySubScore: 0,
13145
13718
  avgCacheHitRate: 0,
13146
13719
  avgIoRatio: 0,
13147
13720
  contextSpikeCount: 0,
13148
13721
  totalTurns: 0,
13722
+ coldStartTurnsExcluded: 0,
13149
13723
  perModelBreakdown: [],
13150
13724
  perSourceBreakdown: []
13151
13725
  };
13152
- const avgCacheHitRate = this._computeAvgCacheHitRate(turns);
13153
- const avgIoRatio = this._computeAvgIoRatio(turns);
13726
+ const taskType = this._inferTaskType(turns);
13727
+ const baseline = getBaseline(taskType);
13728
+ const coldStartIds = this._identifyColdStartTurns(turns);
13729
+ let scoringTurns = turns.filter((t) => !coldStartIds.has(t.spanId));
13730
+ if (scoringTurns.length === 0) scoringTurns = turns;
13731
+ const avgCacheHitRate = this._computeAvgCacheHitRate(scoringTurns);
13732
+ const avgIoRatio = this._computeAvgIoRatio(scoringTurns);
13154
13733
  const contextSpikeCount = turns.filter((t) => t.isContextSpike).length;
13155
13734
  const totalTurns = turns.length;
13156
- const cacheHitSubScore = this._computeCacheHitSubScore(turns);
13157
- const ioRatioSubScore = this._computeIoRatioSubScore(turns);
13158
- const contextManagementSubScore = this._computeContextManagementSubScore(turns);
13159
- const compositeScore = Math.round(cacheHitSubScore * .4 + ioRatioSubScore * .3 + contextManagementSubScore * .3);
13160
- const perModelBreakdown = this._buildPerModelBreakdown(turns);
13161
- const perSourceBreakdown = this._buildPerSourceBreakdown(turns);
13735
+ const cacheHitSubScore = this._computeCacheHitSubScore(scoringTurns);
13736
+ const ioRatioSubScore = this._computeIoRatioSubScore(scoringTurns, baseline.targetIoRatio);
13737
+ const contextManagementSubScore = this._computeContextManagementSubScore(scoringTurns);
13738
+ const tokenDensitySubScore = this._computeTokenDensitySubScore(scoringTurns, baseline.expectedOutputPerTurn);
13739
+ const compositeScore = Math.round(cacheHitSubScore * W_CACHE + ioRatioSubScore * W_IO_RATIO + contextManagementSubScore * W_CONTEXT + tokenDensitySubScore * W_TOKEN_DENSITY);
13740
+ const perModelBreakdown = this._buildPerModelBreakdown(scoringTurns);
13741
+ const perSourceBreakdown = this._buildPerSourceBreakdown(scoringTurns, baseline.targetIoRatio, baseline.expectedOutputPerTurn);
13162
13742
  this._logger.info({
13163
13743
  storyKey,
13164
13744
  compositeScore,
13165
- contextSpikeCount
13745
+ contextSpikeCount,
13746
+ coldStartTurnsExcluded: coldStartIds.size
13166
13747
  }, "Computed efficiency score");
13167
13748
  return {
13168
13749
  storyKey,
@@ -13171,15 +13752,41 @@ var EfficiencyScorer = class {
13171
13752
  cacheHitSubScore,
13172
13753
  ioRatioSubScore,
13173
13754
  contextManagementSubScore,
13755
+ tokenDensitySubScore,
13174
13756
  avgCacheHitRate,
13175
13757
  avgIoRatio,
13176
13758
  contextSpikeCount,
13177
13759
  totalTurns,
13760
+ coldStartTurnsExcluded: coldStartIds.size,
13178
13761
  perModelBreakdown,
13179
13762
  perSourceBreakdown
13180
13763
  };
13181
13764
  }
13182
13765
  /**
13766
+ * Identify cold-start turns: the first turn per dispatchId.
13767
+ * Returns a set of spanIds that should be excluded from scoring.
13768
+ * Only considers turns with a non-empty dispatchId.
13769
+ */
13770
+ _identifyColdStartTurns(turns) {
13771
+ const coldStarts = new Set();
13772
+ const seenDispatches = new Set();
13773
+ for (const turn of turns) if (turn.dispatchId !== void 0 && turn.dispatchId !== "" && !seenDispatches.has(turn.dispatchId)) {
13774
+ seenDispatches.add(turn.dispatchId);
13775
+ coldStarts.add(turn.spanId);
13776
+ }
13777
+ return coldStarts;
13778
+ }
13779
+ /**
13780
+ * Infer the task type from turns. Returns the task type only when all turns
13781
+ * with a taskType agree (unanimous). For mixed task types (story-level
13782
+ * scoring across dispatches), returns undefined → default baseline.
13783
+ */
13784
+ _inferTaskType(turns) {
13785
+ const types$1 = new Set();
13786
+ for (const turn of turns) if (turn.taskType !== void 0 && turn.taskType !== "") types$1.add(turn.taskType);
13787
+ return types$1.size === 1 ? [...types$1][0] : void 0;
13788
+ }
13789
+ /**
13183
13790
  * Average cache hit rate across all turns, clamped to [0, 100].
13184
13791
  * Formula: clamp(avgCacheHitRate × 100, 0, 100)
13185
13792
  */
@@ -13188,23 +13795,26 @@ var EfficiencyScorer = class {
13188
13795
  return this._clamp(avg * 100, 0, 100);
13189
13796
  }
13190
13797
  /**
13191
- * I/O ratio sub-score: measures output productivity.
13798
+ * I/O ratio sub-score: logarithmic output/freshInput productivity curve (Story 35-1).
13799
+ *
13800
+ * Replaces the old binary threshold (>=1 → 100) with a logarithmic curve
13801
+ * that provides gradient across the observed range:
13802
+ * - score = clamp(log10(ratio) / log10(targetRatio) * 100, 0, 100)
13803
+ * - ratio = avg(outputTokens / max(freshInputTokens, 1)) across turns
13804
+ * - targetRatio is calibrated per task type (Story 35-2)
13192
13805
  *
13193
- * For code generation workloads, high context-to-output ratio is normal and
13194
- * desirable (agent reads large cached context, produces substantial code).
13195
- * The old formula penalized this. New formula uses output-to-fresh-input ratio:
13196
- * - outputTokens / max(freshInputTokens, 1) per turn
13197
- * - Ratio > 1 means productive (more output than fresh input) → score 100
13198
- * - Ratio < 1 → scaled linearly: ratio * 100
13199
- * - Averaged across turns
13806
+ * Examples (TARGET=100): ratio 1→0, 10→50, 50→85, 100→100, 200→100(clamped)
13200
13807
  */
13201
- _computeIoRatioSubScore(turns) {
13808
+ _computeIoRatioSubScore(turns, targetRatio) {
13202
13809
  if (turns.length === 0) return 0;
13203
13810
  const avg = turns.reduce((acc, t) => {
13204
13811
  const freshInput = Math.max(t.inputTokens, 1);
13205
13812
  return acc + t.outputTokens / freshInput;
13206
13813
  }, 0) / turns.length;
13207
- return this._clamp(avg >= 1 ? 100 : avg * 100, 0, 100);
13814
+ if (avg <= 0) return 0;
13815
+ const logTarget = Math.log10(Math.max(targetRatio, 2));
13816
+ const score = Math.log10(avg) / logTarget * 100;
13817
+ return this._clamp(score, 0, 100);
13208
13818
  }
13209
13819
  /**
13210
13820
  * Context management sub-score: penalizes context spike frequency.
@@ -13217,6 +13827,22 @@ var EfficiencyScorer = class {
13217
13827
  const spikeRatio = spikeCount / totalTurns;
13218
13828
  return this._clamp(100 - spikeRatio * 100, 0, 100);
13219
13829
  }
13830
+ /**
13831
+ * Token density sub-score: output tokens per turn vs task-type baseline (Story 35-4).
13832
+ *
13833
+ * Measures whether the agent is producing useful output or spinning:
13834
+ * - score = clamp(avgOutputPerTurn / expectedOutputPerTurn * 100, 0, 100)
13835
+ * - expectedOutputPerTurn is calibrated per task type (Story 35-2)
13836
+ *
13837
+ * Below-baseline dispatches get proportionally lower scores.
13838
+ * At-or-above-baseline dispatches score 100.
13839
+ */
13840
+ _computeTokenDensitySubScore(turns, expectedOutputPerTurn) {
13841
+ if (turns.length === 0) return 0;
13842
+ const avgOutput = turns.reduce((acc, t) => acc + t.outputTokens, 0) / turns.length;
13843
+ const ratio = avgOutput / Math.max(expectedOutputPerTurn, 1);
13844
+ return this._clamp(ratio * 100, 0, 100);
13845
+ }
13220
13846
  _computeAvgCacheHitRate(turns) {
13221
13847
  if (turns.length === 0) return 0;
13222
13848
  const sum = turns.reduce((acc, t) => acc + t.cacheHitRate, 0);
@@ -13269,7 +13895,7 @@ var EfficiencyScorer = class {
13269
13895
  * Group turns by source, computing a per-group composite score using the
13270
13896
  * same formula as the overall score. Sources with zero turns are excluded.
13271
13897
  */
13272
- _buildPerSourceBreakdown(turns) {
13898
+ _buildPerSourceBreakdown(turns, targetIoRatio, expectedOutputPerTurn) {
13273
13899
  const groups = new Map();
13274
13900
  for (const turn of turns) {
13275
13901
  const key = turn.source;
@@ -13280,10 +13906,11 @@ var EfficiencyScorer = class {
13280
13906
  const result = [];
13281
13907
  for (const [source, groupTurns] of groups) {
13282
13908
  if (groupTurns.length === 0) continue;
13283
- const cacheHitSub = this._computeCacheHitSubScoreForGroup(groupTurns);
13284
- const ioRatioSub = this._computeIoRatioSubScoreForGroup(groupTurns);
13285
- const contextSub = this._computeContextManagementSubScoreForGroup(groupTurns);
13286
- const compositeScore = Math.round(cacheHitSub * .4 + ioRatioSub * .3 + contextSub * .3);
13909
+ const cacheHitSub = this._computeCacheHitSubScore(groupTurns);
13910
+ const ioRatioSub = this._computeIoRatioSubScore(groupTurns, targetIoRatio);
13911
+ const contextSub = this._computeContextManagementSubScore(groupTurns);
13912
+ const tokenDensitySub = this._computeTokenDensitySubScore(groupTurns, expectedOutputPerTurn);
13913
+ const compositeScore = Math.round(cacheHitSub * W_CACHE + ioRatioSub * W_IO_RATIO + contextSub * W_CONTEXT + tokenDensitySub * W_TOKEN_DENSITY);
13287
13914
  result.push({
13288
13915
  source,
13289
13916
  compositeScore,
@@ -13292,25 +13919,6 @@ var EfficiencyScorer = class {
13292
13919
  }
13293
13920
  return result;
13294
13921
  }
13295
- _computeCacheHitSubScoreForGroup(turns) {
13296
- if (turns.length === 0) return 0;
13297
- const avg = turns.reduce((acc, t) => acc + t.cacheHitRate, 0) / turns.length;
13298
- return this._clamp(avg * 100, 0, 100);
13299
- }
13300
- _computeIoRatioSubScoreForGroup(turns) {
13301
- if (turns.length === 0) return 0;
13302
- const avg = turns.reduce((acc, t) => {
13303
- const freshInput = Math.max(t.inputTokens, 1);
13304
- return acc + t.outputTokens / freshInput;
13305
- }, 0) / turns.length;
13306
- return this._clamp(avg >= 1 ? 100 : avg * 100, 0, 100);
13307
- }
13308
- _computeContextManagementSubScoreForGroup(turns) {
13309
- if (turns.length === 0) return 0;
13310
- const spikeCount = turns.filter((t) => t.isContextSpike).length;
13311
- const spikeRatio = spikeCount / turns.length;
13312
- return this._clamp(100 - spikeRatio * 100, 0, 100);
13313
- }
13314
13922
  _clamp(value, min, max) {
13315
13923
  return Math.max(min, Math.min(max, value));
13316
13924
  }
@@ -15165,8 +15773,10 @@ var TelemetryAdvisor = class {
15165
15773
  cacheHitSubScore: score.cacheHitSubScore,
15166
15774
  ioRatioSubScore: score.ioRatioSubScore,
15167
15775
  contextManagementSubScore: score.contextManagementSubScore,
15776
+ tokenDensitySubScore: score.tokenDensitySubScore ?? 0,
15168
15777
  totalTurns: score.totalTurns,
15169
- contextSpikeCount: score.contextSpikeCount
15778
+ contextSpikeCount: score.contextSpikeCount,
15779
+ coldStartTurnsExcluded: score.coldStartTurnsExcluded ?? 0
15170
15780
  };
15171
15781
  } catch (err) {
15172
15782
  logger$6.warn({
@@ -22691,4 +23301,4 @@ function registerRunCommand(program, _version = "0.0.0", projectRoot = process.c
22691
23301
 
22692
23302
  //#endregion
22693
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 };
22694
- //# sourceMappingURL=run-DCW67EQW.js.map
23304
+ //# sourceMappingURL=run-IDOmPys1.js.map