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
|
|
7063
|
-
*
|
|
7064
|
-
*
|
|
7065
|
-
*
|
|
7066
|
-
*
|
|
7067
|
-
*
|
|
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
|
-
*
|
|
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/
|
|
9529
|
-
|
|
9530
|
-
|
|
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 =
|
|
9658
|
-
logger$16.debug({ storyKey }, "No test-pattern decisions
|
|
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 =
|
|
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
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
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
|
|
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
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
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
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
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 (
|
|
12075
|
-
const
|
|
12560
|
+
if (tscFailed) {
|
|
12561
|
+
const truncatedOutput = tscOutput.slice(0, 1e3);
|
|
12562
|
+
const matchedExports = new Set();
|
|
12076
12563
|
for (const exp of exports) {
|
|
12077
|
-
|
|
12078
|
-
if (
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
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
|
-
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
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
|
|
13153
|
-
const
|
|
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(
|
|
13157
|
-
const ioRatioSubScore = this._computeIoRatioSubScore(
|
|
13158
|
-
const contextManagementSubScore = this._computeContextManagementSubScore(
|
|
13159
|
-
const
|
|
13160
|
-
const
|
|
13161
|
-
const
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
13284
|
-
const ioRatioSub = this.
|
|
13285
|
-
const contextSub = this.
|
|
13286
|
-
const
|
|
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-
|
|
23304
|
+
//# sourceMappingURL=run-IDOmPys1.js.map
|