@polymorphism-tech/morph-spec 8.47.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { execSync, execFileSync } from 'child_process';
|
|
31
|
-
import { existsSync, readFileSync, readdirSync, realpathSync } from 'fs';
|
|
31
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'fs';
|
|
32
32
|
import { join, resolve, relative, isAbsolute } from 'path';
|
|
33
33
|
import { globSync } from 'glob';
|
|
34
34
|
|
|
@@ -170,11 +170,6 @@ function collectDotnetTestProjects(projectPath) {
|
|
|
170
170
|
* says "not inside" for the same directory and a linked worktree leaks into
|
|
171
171
|
* the test command. Only `realpathSync.native` asks the OS. Fail-open to the
|
|
172
172
|
* plain resolve for a path that does not exist (yet).
|
|
173
|
-
*
|
|
174
|
-
* Exported because test-scope.js spells every task output with it (#87 F4): a
|
|
175
|
-
* mis-cased output resolves on a case-insensitive disk and was forwarded to the
|
|
176
|
-
* runner verbatim — and the `--linux` container is case-sensitive. The OS also
|
|
177
|
-
* returns the real CASE of each component, so this one call is both answers.
|
|
178
173
|
*/
|
|
179
174
|
export function canonicalPath(p) {
|
|
180
175
|
try {
|
|
@@ -338,6 +333,75 @@ export function resolveTestBuildTargets(node) {
|
|
|
338
333
|
return extractCsprojTargets(node.command);
|
|
339
334
|
}
|
|
340
335
|
|
|
336
|
+
const DOTNET_BUILDABLE = /"([^"]+\.(?:slnx?|csproj))"|'([^']+\.(?:slnx?|csproj))'|(\S+\.(?:slnx?|csproj))/gi;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* What `dotnet test` compiles for this node, relative to its `cwd`: declared `build`,
|
|
340
|
+
* else what the command names, else the directory's single solution/project; `[]` = undiscoverable.
|
|
341
|
+
*/
|
|
342
|
+
export function discoverDotnetBuildTargets(node, cwd) {
|
|
343
|
+
if (node?.runner !== 'dotnet') return [];
|
|
344
|
+
if (Array.isArray(node.build) && node.build.length > 0) return node.build;
|
|
345
|
+
const command = typeof node.command === 'string' ? node.command : '';
|
|
346
|
+
const named = [...command.matchAll(DOTNET_BUILDABLE)].map((m) => m[1] ?? m[2] ?? m[3]);
|
|
347
|
+
if (named.length > 0) return [...new Set(named)];
|
|
348
|
+
|
|
349
|
+
const dirArg = command.split(/\s+/).find((token) => token && !token.startsWith('-') && token !== 'dotnet' && token !== 'test'
|
|
350
|
+
&& isDirectory(join(cwd, token)));
|
|
351
|
+
const dir = dirArg ?? '.';
|
|
352
|
+
let entries;
|
|
353
|
+
try { entries = readdirSync(join(cwd, dir)); } catch { return []; }
|
|
354
|
+
const only = singleDotnetBuildable(entries);
|
|
355
|
+
return only ? [dir === '.' ? only : join(dir, only).replace(/\\/g, '/')] : [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The one solution among `files`, else the one project; `null` when `dotnet build` would have to guess. */
|
|
359
|
+
function singleDotnetBuildable(files) {
|
|
360
|
+
const solutions = files.filter((f) => /\.slnx?$/i.test(f));
|
|
361
|
+
const projects = files.filter((f) => /\.csproj$/i.test(f));
|
|
362
|
+
if (solutions.length) return solutions.length === 1 ? solutions[0] : null;
|
|
363
|
+
return projects.length === 1 ? projects[0] : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const DOTNET_SCAN_SKIPPED = new Set([WORKTREES_DIR, LEGACY_WORKTREES_DIR, 'node_modules', 'bin', 'obj']);
|
|
367
|
+
const DOTNET_SCAN_DEPTH = 6;
|
|
368
|
+
|
|
369
|
+
function dotnetBuildablesUnder(root, rel = '', depth = 0, found = []) {
|
|
370
|
+
let entries;
|
|
371
|
+
try { entries = readdirSync(join(root, rel), { withFileTypes: true }); } catch { return found; }
|
|
372
|
+
for (const e of entries) {
|
|
373
|
+
const path = rel ? `${rel}/${e.name}` : e.name;
|
|
374
|
+
if (e.isFile() && /\.(?:slnx?|csproj)$/i.test(e.name)) found.push(path);
|
|
375
|
+
else if (e.isDirectory() && depth < DOTNET_SCAN_DEPTH && !e.name.startsWith('.') && !DOTNET_SCAN_SKIPPED.has(e.name)) {
|
|
376
|
+
dotnetBuildablesUnder(root, path, depth + 1, found);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return found;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* What a root `dotnet build` compiles when the root itself holds no solution or
|
|
384
|
+
* project: the repository's single .sln/.slnx, else its single .csproj, never
|
|
385
|
+
* looking inside worktree containers, node_modules, bin or obj.
|
|
386
|
+
*
|
|
387
|
+
* @returns {{ target: string|null, candidates: string[] }} `target` relative to
|
|
388
|
+
* `root` (`.` when the root already has one); `null` for zero or several, listed.
|
|
389
|
+
*/
|
|
390
|
+
export function discoverDotnetSolution(root) {
|
|
391
|
+
let rootEntries;
|
|
392
|
+
try { rootEntries = readdirSync(root); } catch { return { target: null, candidates: [] }; }
|
|
393
|
+
if (rootEntries.some((e) => /\.(?:slnx?|csproj)$/i.test(e))) return { target: '.', candidates: [] };
|
|
394
|
+
const all = dotnetBuildablesUnder(root).sort();
|
|
395
|
+
const solutions = all.filter((f) => /\.slnx?$/i.test(f));
|
|
396
|
+
const pool = solutions.length ? solutions : all;
|
|
397
|
+
const single = singleDotnetBuildable(pool);
|
|
398
|
+
return { target: single, candidates: pool };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isDirectory(path) {
|
|
402
|
+
try { return statSync(path).isDirectory(); } catch { return false; }
|
|
403
|
+
}
|
|
404
|
+
|
|
341
405
|
/**
|
|
342
406
|
* `dotnet build "<target>" …`, built from the SAME step build-check.js
|
|
343
407
|
* resolves for a .NET stack — the string "dotnet build" is not typed a second
|
|
@@ -488,29 +552,6 @@ function parseDotnetCount(output) {
|
|
|
488
552
|
return seen ? sum : null;
|
|
489
553
|
}
|
|
490
554
|
|
|
491
|
-
/**
|
|
492
|
-
* The `total` of every MTP summary block in `output`, ANSI stripped — one per
|
|
493
|
-
* `dotnet test` invocation (measured: a solution prints ONE aggregated block).
|
|
494
|
-
* `[]` when none is readable. Read by `judgeNarrowedRun` (verify-runner.js): in a
|
|
495
|
-
* narrowed `&&` chain the SUM can be > 0 while one link ran nothing — its exit 8
|
|
496
|
-
* ignored by a path the scanner cannot read — and that link's own block says
|
|
497
|
-
* `total: 0`.
|
|
498
|
-
*
|
|
499
|
-
* @param {string|null|undefined} output
|
|
500
|
-
* @returns {number[]}
|
|
501
|
-
*/
|
|
502
|
-
export function mtpSummaryTotals(output) {
|
|
503
|
-
if (typeof output !== 'string' || !output) return [];
|
|
504
|
-
const lines = stripAnsi(output).split('\n');
|
|
505
|
-
const totals = [];
|
|
506
|
-
lines.forEach((line, i) => {
|
|
507
|
-
if (!MTP_SUMMARY_HEADER.test(line)) return;
|
|
508
|
-
const { total } = mtpSummaryBlock(lines, i + 1);
|
|
509
|
-
if (total !== null) totals.push(total);
|
|
510
|
-
});
|
|
511
|
-
return totals;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
555
|
/** Last value of a `# key N` (TAP) or `ℹ key N` (spec reporter) summary line. */
|
|
515
556
|
function lastSummaryValue(output, key) {
|
|
516
557
|
const re = new RegExp(`^\\s*[#ℹ]\\s*${key}\\s+(\\d+)\\s*$`, 'gim');
|
|
@@ -578,8 +619,8 @@ const ANSI_PATTERN = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/
|
|
|
578
619
|
/**
|
|
579
620
|
* Strip ANSI escape sequences (CSI/SGR and OSC) from captured process output.
|
|
580
621
|
*
|
|
581
|
-
* Non-string input is returned UNCHANGED, never coerced to `''` — the
|
|
582
|
-
* that
|
|
622
|
+
* Non-string input is returned UNCHANGED, never coerced to `''` — the caller
|
|
623
|
+
* that matters (`parseTestCount`) already treats a non-string
|
|
583
624
|
* output as "nothing to read" on their own; making this function opinionated
|
|
584
625
|
* about that too would be a second place for the same rule to drift.
|
|
585
626
|
*
|
|
@@ -640,91 +681,6 @@ export function parseTestCount(output, runner) {
|
|
|
640
681
|
return extractor(stripAnsi(output));
|
|
641
682
|
}
|
|
642
683
|
|
|
643
|
-
// ============================================================================
|
|
644
|
-
// "Zero tests ran" signatures, per runner (#87 PR B / #110 parte 1)
|
|
645
|
-
// ============================================================================
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Per-runner signal for "the run matched/executed NOTHING" — a category
|
|
649
|
-
* distinct from both a genuine `{ total: 0, ... }` summary (a suite that
|
|
650
|
-
* legitimately declares zero cases) and from a summary `parseTestCount`
|
|
651
|
-
* simply could not read (`null`, UNKNOWN). Its reader is `judgeNarrowedRun`
|
|
652
|
-
* (verify-runner.js, #87): a narrowed `verify {feature} {task}` run that FAILED
|
|
653
|
-
* with this signature, and with no count proving tests ran, is named
|
|
654
|
-
* `narrowed-zero-tests (…)` instead of an anonymous red. (A PASSED narrowed run
|
|
655
|
-
* needs its own proof there — a count > 0, or MTP's exit 0 — never this table.)
|
|
656
|
-
*
|
|
657
|
-
* Every entry was MEASURED against a real runner, not invented, and the two
|
|
658
|
-
* dotnet entries disagree with each other on purpose:
|
|
659
|
-
*
|
|
660
|
-
* - **MTP** (`--filter-class`, this repo's own `ai-kit`, xunit.v3 4.x):
|
|
661
|
-
* a non-matching filter exits **8** (`ZeroTests`) — reliable BY EXIT
|
|
662
|
-
* CODE. See `test/fixtures/test-output/mtp-zero-tests.txt`.
|
|
663
|
-
* - **VSTest** (`--filter "FullyQualifiedName~..."`, classic xunit 2.x):
|
|
664
|
-
* a non-matching filter exits **0** — exit code CANNOT tell a zero-match
|
|
665
|
-
* run from a green one here. Only the OUTPUT STRING can:
|
|
666
|
-
* `test/fixtures/test-output/vstest-zero-tests.txt` (pt-BR, measured).
|
|
667
|
-
* The en-US wording is the brief's documented string, not measured on
|
|
668
|
-
* this pt-BR machine — kept alongside the pt-BR one because neither is
|
|
669
|
-
* derivable from the other and the machine that runs `verify` may be in
|
|
670
|
-
* either locale.
|
|
671
|
-
*
|
|
672
|
-
* Exit code 8 is therefore listed ONLY as a dotnet exit code, never paired
|
|
673
|
-
* with a string requirement — and exit code 9
|
|
674
|
-
* (`MinimumExpectedTestsPolicyViolation`, this repo's own
|
|
675
|
-
* `--minimum-expected-tests 275`) is deliberately ABSENT: it fires on a run
|
|
676
|
-
* that executed fewer cases than the configured floor, including a run that
|
|
677
|
-
* was entirely green (measured:
|
|
678
|
-
* `test/fixtures/test-output/mtp-filter-class-policy-violation.txt`, 10/10
|
|
679
|
-
* passed, exit 9). Folding 9 into this table would misreport a real pass as
|
|
680
|
-
* "zero tests ran".
|
|
681
|
-
*/
|
|
682
|
-
export const ZERO_RUN_SIGNATURES = {
|
|
683
|
-
dotnet: {
|
|
684
|
-
exitCodes: [8],
|
|
685
|
-
strings: [
|
|
686
|
-
// en-US, VSTest — documented by the brief, not measured here (pt-BR machine).
|
|
687
|
-
'No test matches the given testcase filter',
|
|
688
|
-
// pt-BR, VSTest — MEASURED, test/fixtures/test-output/vstest-zero-tests.txt.
|
|
689
|
-
'Nenhum teste corresponde ao filtro de testcase',
|
|
690
|
-
],
|
|
691
|
-
},
|
|
692
|
-
vitest: {
|
|
693
|
-
exitCodes: [],
|
|
694
|
-
strings: ['No test files found'],
|
|
695
|
-
},
|
|
696
|
-
jest: {
|
|
697
|
-
exitCodes: [],
|
|
698
|
-
strings: ['No tests found'],
|
|
699
|
-
},
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* True when `output`/`exitCode` carries this runner's "zero tests ran"
|
|
704
|
-
* signature. Every other runner (`node-test`, `playwright`, `evals`, `none`,
|
|
705
|
-
* or anything not in `ZERO_RUN_SIGNATURES`) answers `false` — an absent entry
|
|
706
|
-
* is an honest "this primitive does not know that runner's zero-tests shape
|
|
707
|
-
* yet", the same doctrine `COUNT_EXTRACTORS` uses for an unknown runner.
|
|
708
|
-
*
|
|
709
|
-
* Called by `judgeNarrowedRun` (verify-runner.js) on a FAILED narrowed run, to
|
|
710
|
-
* name it `narrowed-zero-tests (…)`.
|
|
711
|
-
*
|
|
712
|
-
* @param {string|null|undefined} output - Combined stdout+stderr of the run
|
|
713
|
-
* @param {string} runner - One of TEST_RUNNERS
|
|
714
|
-
* @param {number|null|undefined} exitCode - Must be a NUMBER to match: the
|
|
715
|
-
* exit-code check is a strict `includes`, so the string `'8'` never matches 8.
|
|
716
|
-
* @returns {boolean}
|
|
717
|
-
*/
|
|
718
|
-
export function isZeroTestRun(output, runner, exitCode) {
|
|
719
|
-
const signature = ZERO_RUN_SIGNATURES[runner];
|
|
720
|
-
if (!signature) return false;
|
|
721
|
-
|
|
722
|
-
if (signature.exitCodes.includes(exitCode)) return true;
|
|
723
|
-
|
|
724
|
-
const text = stripAnsi(typeof output === 'string' ? output : '');
|
|
725
|
-
return signature.strings.some((needle) => text.includes(needle));
|
|
726
|
-
}
|
|
727
|
-
|
|
728
684
|
/**
|
|
729
685
|
* Compare an executed count against the recorded baseline — asymmetrically,
|
|
730
686
|
* and on purpose (D7).
|
|
@@ -840,7 +796,9 @@ export function runTestSuite(projectPath = '.', opts = {}) {
|
|
|
840
796
|
});
|
|
841
797
|
return { passed: true, output: output ?? '', exitCode: 0, timeoutMs: timeout };
|
|
842
798
|
} catch (err) {
|
|
843
|
-
|
|
799
|
+
// A spawn failure or a kill leaves both streams empty; its own error is then the only lead.
|
|
800
|
+
const output = (err.stdout || '') + (err.stderr || '')
|
|
801
|
+
|| [err.code, err.signal, err.message].filter(Boolean).join(' ');
|
|
844
802
|
|
|
845
803
|
// A timeout is NOT a red suite: the command was interrupted, so no verdict
|
|
846
804
|
// exists. Saying so by name is the whole point — the field diagnosis of
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Loop measurements over the harness event stream: turns, tool calls split by
|
|
3
3
|
* orchestrator and sub-agent, time per command class, identical reruns, file
|
|
4
|
-
* rereads,
|
|
5
|
-
*
|
|
4
|
+
* rereads, verify count per feature, git calls and whether the reuse
|
|
5
|
+
* validator ran. Computed the same way on every window so a later
|
|
6
6
|
* window compares with an earlier one.
|
|
7
7
|
*
|
|
8
8
|
* Every number degrades to null ("not measurable here") when the stream lacks
|
|
@@ -43,8 +43,7 @@ function executedText(command) {
|
|
|
43
43
|
*/
|
|
44
44
|
export function classifyCommand(command) {
|
|
45
45
|
const cmd = String(command || '');
|
|
46
|
-
|
|
47
|
-
if (verify) return { kind: verify[1] ? 'verify-task' : 'verify-feature', klass: 'verify' };
|
|
46
|
+
if (/\bmorph-spec(?:\.js)?\s+verify\s+[^\s;&|)]+/.test(executedText(cmd))) return { kind: 'verify', klass: 'verify' };
|
|
48
47
|
if (/\bdotnet\s+test\b/.test(cmd)) {
|
|
49
48
|
if (/--list-tests\b/.test(cmd)) return { kind: 'dotnet-test-list', klass: 'test' };
|
|
50
49
|
return { kind: /--filter\b/.test(cmd) ? 'dotnet-test-filtered' : 'dotnet-test-full', klass: 'test' };
|
|
@@ -159,8 +158,6 @@ export function computeMetrics(events, opts = {}) {
|
|
|
159
158
|
|
|
160
159
|
const global = newCounters();
|
|
161
160
|
const byFeature = new Map();
|
|
162
|
-
const openTask = new Map();
|
|
163
|
-
const tasks = [];
|
|
164
161
|
const lastRun = new Map();
|
|
165
162
|
const editCount = new Map();
|
|
166
163
|
const readsSeen = new Set();
|
|
@@ -172,8 +169,6 @@ export function computeMetrics(events, opts = {}) {
|
|
|
172
169
|
const lastFingerprint = new Map();
|
|
173
170
|
const unchangedRepeats = {};
|
|
174
171
|
const reuse = { runs: 0, byTree: {}, skipReasons: {} };
|
|
175
|
-
const tasksOf = new Map();
|
|
176
|
-
let callsInTaskWindows = 0;
|
|
177
172
|
let v2 = 0;
|
|
178
173
|
let legacy = 0;
|
|
179
174
|
let turnsObserved = false;
|
|
@@ -184,16 +179,6 @@ export function computeMetrics(events, opts = {}) {
|
|
|
184
179
|
if (!byFeature.has(key)) byFeature.set(key, newCounters());
|
|
185
180
|
return byFeature.get(key);
|
|
186
181
|
};
|
|
187
|
-
const closeTask = (key, endTs, status) => {
|
|
188
|
-
const task = openTask.get(key);
|
|
189
|
-
if (!task) return;
|
|
190
|
-
openTask.delete(key);
|
|
191
|
-
task.endTs = endTs;
|
|
192
|
-
task.status = status;
|
|
193
|
-
task.open = endTs === null;
|
|
194
|
-
tasks.push(task);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
182
|
for (const e of scoped) {
|
|
198
183
|
if (isV2(e)) v2 += 1; else legacy += 1;
|
|
199
184
|
if (e.ts) {
|
|
@@ -202,21 +187,7 @@ export function computeMetrics(events, opts = {}) {
|
|
|
202
187
|
}
|
|
203
188
|
const fkey = e.feature || NO_FEATURE;
|
|
204
189
|
|
|
205
|
-
if (e.event === 'TaskStart' && e.task) {
|
|
206
|
-
if (!tasksOf.has(fkey)) tasksOf.set(fkey, new Set());
|
|
207
|
-
tasksOf.get(fkey).add(e.task);
|
|
208
|
-
closeTask(fkey, null, null);
|
|
209
|
-
openTask.set(fkey, { feature: e.feature ?? null, task: e.task, startTs: e.ts, endTs: null, status: null, open: true, c: newCounters() });
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (e.event === 'TaskEnd' && e.task) {
|
|
213
|
-
if (openTask.get(fkey)?.task === e.task) closeTask(fkey, e.ts, e.status ?? null);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
190
|
const sinks = [global, featureCounters(fkey)];
|
|
218
|
-
const task = openTask.get(fkey);
|
|
219
|
-
if (task) sinks.push(task.c);
|
|
220
191
|
|
|
221
192
|
if (e.event === 'ToolBatch') {
|
|
222
193
|
turnsObserved = true;
|
|
@@ -225,7 +196,7 @@ export function computeMetrics(events, opts = {}) {
|
|
|
225
196
|
}
|
|
226
197
|
|
|
227
198
|
if (e.event === 'Verify') {
|
|
228
|
-
if (e.
|
|
199
|
+
if (e.fingerprint && e.persisted !== false) {
|
|
229
200
|
if (lastFingerprint.get(fkey) === e.fingerprint) {
|
|
230
201
|
unchangedRepeats[e.feature || '(sem feature)'] = (unchangedRepeats[e.feature || '(sem feature)'] || 0) + 1;
|
|
231
202
|
}
|
|
@@ -242,7 +213,6 @@ export function computeMetrics(events, opts = {}) {
|
|
|
242
213
|
|
|
243
214
|
if (!TOOL_EVENTS.has(e.event) || !e.tool) continue;
|
|
244
215
|
for (const c of sinks) { c.toolCalls.total += 1; c.toolCalls[whoOf(e)] += 1; }
|
|
245
|
-
if (task) callsInTaskWindows += 1;
|
|
246
216
|
|
|
247
217
|
if (e.tool === 'Write' || e.tool === 'Edit') {
|
|
248
218
|
if (!isFailedToolCall(e)) editCount.set(fkey, (editCount.get(fkey) || 0) + 1);
|
|
@@ -288,8 +258,8 @@ export function computeMetrics(events, opts = {}) {
|
|
|
288
258
|
}
|
|
289
259
|
|
|
290
260
|
if (klass === 'verify') {
|
|
291
|
-
const
|
|
292
|
-
|
|
261
|
+
const name = e.feature || '(sem feature)';
|
|
262
|
+
verifyByFeature[name] = (verifyByFeature[name] || 0) + 1;
|
|
293
263
|
}
|
|
294
264
|
|
|
295
265
|
if (!RERUN_CLASSES.has(klass) || typeof e.command !== 'string') continue;
|
|
@@ -318,34 +288,15 @@ export function computeMetrics(events, opts = {}) {
|
|
|
318
288
|
lastRun.set(runKey, run);
|
|
319
289
|
}
|
|
320
290
|
|
|
321
|
-
for (const key of [...openTask.keys()]) closeTask(key, null, null);
|
|
322
|
-
tasks.sort((a, b) => String(a.startTs).localeCompare(String(b.startTs)));
|
|
323
|
-
|
|
324
|
-
const taskRows = tasks.map((t) => ({
|
|
325
|
-
feature: t.feature,
|
|
326
|
-
task: t.task,
|
|
327
|
-
startTs: t.startTs,
|
|
328
|
-
endTs: t.endTs,
|
|
329
|
-
open: t.open,
|
|
330
|
-
status: t.status,
|
|
331
|
-
...finishCounters(t.c, turnsObserved),
|
|
332
|
-
}));
|
|
333
|
-
|
|
334
291
|
const featureRows = [...byFeature.entries()]
|
|
335
292
|
.filter(([key]) => key !== NO_FEATURE)
|
|
336
293
|
.map(([feature, c]) => ({
|
|
337
294
|
feature,
|
|
338
295
|
...finishCounters(c, turnsObserved),
|
|
339
|
-
verify: verifyByFeature[feature] ||
|
|
296
|
+
verify: verifyByFeature[feature] || 0,
|
|
340
297
|
}))
|
|
341
298
|
.sort((a, b) => b.toolCalls.total - a.toolCalls.total || a.feature.localeCompare(b.feature));
|
|
342
299
|
|
|
343
|
-
// A task window only holds what ran between its TaskStart and TaskEnd, and
|
|
344
|
-
// a parallel batch or a late `in_progress` leaves most calls outside any
|
|
345
|
-
// window. Dividing each feature's totals by its task count needs no window.
|
|
346
|
-
const perTaskOfFeature = (pick) => median(featureRows
|
|
347
|
-
.filter((f) => tasksOf.has(f.feature))
|
|
348
|
-
.map((f) => Math.round((pick(f) / tasksOf.get(f.feature).size) * 10) / 10));
|
|
349
300
|
const byKind = {};
|
|
350
301
|
for (const [kind, slot] of Object.entries(rerunByKind)) {
|
|
351
302
|
byKind[kind] = { ...slot, rate: rate(slot.reruns, slot.runs), burstRate: rate(slot.inBursts, slot.runs) };
|
|
@@ -364,23 +315,11 @@ export function computeMetrics(events, opts = {}) {
|
|
|
364
315
|
.slice(0, top),
|
|
365
316
|
verify: {
|
|
366
317
|
byFeature: verifyByFeature,
|
|
367
|
-
|
|
318
|
+
median: median(featureRows.map((f) => f.verify).filter((n) => n > 0)),
|
|
368
319
|
unchangedRepeats,
|
|
369
320
|
},
|
|
370
|
-
gitPerTaskMedian: {
|
|
371
|
-
status: median(taskRows.map((t) => t.git.status)),
|
|
372
|
-
diff: median(taskRows.map((t) => t.git.diff)),
|
|
373
|
-
},
|
|
374
|
-
turnsPerTaskMedian: turnsObserved ? median(taskRows.map((t) => t.turns.total)) : null,
|
|
375
|
-
taskWindowCoverage: rate(callsInTaskWindows, global.toolCalls.total),
|
|
376
|
-
perTaskByFeature: {
|
|
377
|
-
git: perTaskOfFeature((f) => f.git.status + f.git.diff),
|
|
378
|
-
toolCalls: perTaskOfFeature((f) => f.toolCalls.total),
|
|
379
|
-
turns: turnsObserved ? perTaskOfFeature((f) => f.turns.total) : null,
|
|
380
|
-
},
|
|
381
321
|
dotnetFullSuite: fullSuite,
|
|
382
322
|
reuse,
|
|
383
323
|
features: featureRows,
|
|
384
|
-
tasks: taskRows,
|
|
385
324
|
};
|
|
386
325
|
}
|
|
@@ -232,10 +232,9 @@ export function aggregateTelemetry(events, opts = {}) {
|
|
|
232
232
|
// test/commands/telemetry.test.js, which spreads it wholesale into --json
|
|
233
233
|
// output — adding fields there would leak into that contract).
|
|
234
234
|
//
|
|
235
|
-
// Turns the same raw event stream into a cost-facing view: how
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// (and how many prompt chars they cost).
|
|
235
|
+
// Turns the same raw event stream into a cost-facing view: how many context
|
|
236
|
+
// bytes were re-ingested (by kind, globally and per phase), and how many
|
|
237
|
+
// sub-agent dispatches happened (and how many prompt chars they cost).
|
|
239
238
|
//
|
|
240
239
|
// "Bytes re-ingested" is a DETERMINISTIC PROXY for cost, not a token count —
|
|
241
240
|
// hooks can only see what tools read off disk, never the model's actual
|
|
@@ -243,35 +242,6 @@ export function aggregateTelemetry(events, opts = {}) {
|
|
|
243
242
|
// computes the numbers.
|
|
244
243
|
// ─────────────────────────────────────────────────────────────────────────
|
|
245
244
|
|
|
246
|
-
// Sentinel used to key the open-task map when an event carries no feature
|
|
247
|
-
// (e.g. telemetry-log's getActiveFeature() found none). Without this, a
|
|
248
|
-
// TaskStart with feature: null would silently fail to open (map keyed on
|
|
249
|
-
// undefined/null never matches itself across two different `null`-feature
|
|
250
|
-
// pairs is fine, but `if (e.feature)` truthiness checks would just drop the
|
|
251
|
-
// event) — keying explicitly avoids that class of "counter never increments"
|
|
252
|
-
// bug.
|
|
253
|
-
const NO_FEATURE = '\u0000no-feature';
|
|
254
|
-
|
|
255
|
-
function featureKey(e) {
|
|
256
|
-
return e && e.feature ? e.feature : NO_FEATURE;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function tsToMs(ts) {
|
|
260
|
-
const t = Date.parse(ts);
|
|
261
|
-
return Number.isNaN(t) ? null : t;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Seconds between two ISO timestamps, rounded to millisecond precision to
|
|
266
|
-
* absorb float noise from the division. null if either ts is unparsable.
|
|
267
|
-
*/
|
|
268
|
-
function durationSeconds(startTs, endTs) {
|
|
269
|
-
const a = tsToMs(startTs);
|
|
270
|
-
const b = tsToMs(endTs);
|
|
271
|
-
if (a === null || b === null) return null;
|
|
272
|
-
return Math.round(((b - a) / 1000) * 1000) / 1000;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
245
|
function mapToObj(map) {
|
|
276
246
|
const out = {};
|
|
277
247
|
for (const key of [...map.keys()].sort()) out[key] = map.get(key);
|
|
@@ -284,52 +254,9 @@ function sumValues(map) {
|
|
|
284
254
|
return total;
|
|
285
255
|
}
|
|
286
256
|
|
|
287
|
-
function newTaskState(feature, task, ts) {
|
|
288
|
-
return {
|
|
289
|
-
feature: feature ?? null,
|
|
290
|
-
task,
|
|
291
|
-
startTs: ts,
|
|
292
|
-
endTs: null,
|
|
293
|
-
status: null,
|
|
294
|
-
open: true,
|
|
295
|
-
tools: new Map(),
|
|
296
|
-
bytesByKind: new Map(),
|
|
297
|
-
dispatches: { count: 0, promptChars: 0 },
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function finalizeTask(state) {
|
|
302
|
-
return {
|
|
303
|
-
feature: state.feature,
|
|
304
|
-
task: state.task,
|
|
305
|
-
startTs: state.startTs,
|
|
306
|
-
endTs: state.endTs,
|
|
307
|
-
durationSeconds: state.endTs ? durationSeconds(state.startTs, state.endTs) : null,
|
|
308
|
-
status: state.status,
|
|
309
|
-
open: state.open,
|
|
310
|
-
tools: rank(state.tools),
|
|
311
|
-
bytesByKind: mapToObj(state.bytesByKind),
|
|
312
|
-
bytesTotal: sumValues(state.bytesByKind),
|
|
313
|
-
dispatches: { ...state.dispatches },
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
257
|
/**
|
|
318
|
-
* Summarize an event stream for cost purposes:
|
|
319
|
-
* per
|
|
320
|
-
* `kind` (global, per task, per phase), and sub-agent dispatch counts.
|
|
321
|
-
*
|
|
322
|
-
* Segmentation model: a task is "open" for a feature from its TaskStart
|
|
323
|
-
* event until the matching TaskEnd (same feature + task name). Only one task
|
|
324
|
-
* is assumed open per feature at a time — matches how the implement phase
|
|
325
|
-
* actually runs (auto-advance processes tasks sequentially, never two tasks
|
|
326
|
-
* of the same feature concurrently). Events for a feature with no open task
|
|
327
|
-
* simply don't attribute to any task (they still count toward the
|
|
328
|
-
* global/phase totals). A TaskEnd with no matching open TaskStart is ignored
|
|
329
|
-
* — a truncated stream or a legacy event mix never throws. A task left open
|
|
330
|
-
* at the end of the stream (TaskStart with no TaskEnd yet) is still reported,
|
|
331
|
-
* with `open: true` and `durationSeconds: null`, rather than silently
|
|
332
|
-
* dropped.
|
|
258
|
+
* Summarize an event stream for cost purposes: bytes-re-ingested buckets by
|
|
259
|
+
* `kind` (global and per phase) and sub-agent dispatch counts.
|
|
333
260
|
*
|
|
334
261
|
* Byte attribution prefers `bytesRead` (the line window a Read put in context)
|
|
335
262
|
* and falls back to `bytes` (the file size — all v1 events have) — keyed off
|
|
@@ -356,7 +283,6 @@ function finalizeTask(state) {
|
|
|
356
283
|
* @returns {{
|
|
357
284
|
* total: number,
|
|
358
285
|
* features: string[],
|
|
359
|
-
* taskDurationSeconds: number,
|
|
360
286
|
* bytesByKind: Object<string, number>,
|
|
361
287
|
* bytesTotal: number,
|
|
362
288
|
* dispatches: {count:number, promptChars:number},
|
|
@@ -364,7 +290,6 @@ function finalizeTask(state) {
|
|
|
364
290
|
* sessionUsageLegacySamples: number,
|
|
365
291
|
* agentUsage: {inputTokens:number, outputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, agents:number, samples:number}|null,
|
|
366
292
|
* tools: Array<{name:string,count:number}>,
|
|
367
|
-
* tasks: Array<Object>,
|
|
368
293
|
* phases: Array<{phase:string, bytesByKind:Object<string,number>, bytesTotal:number}>,
|
|
369
294
|
* }}
|
|
370
295
|
*/
|
|
@@ -388,8 +313,6 @@ export function aggregateCost(events, opts = {}) {
|
|
|
388
313
|
const bytesByKindGlobal = new Map();
|
|
389
314
|
const dispatches = { count: 0, promptChars: 0 };
|
|
390
315
|
const phaseBytes = new Map(); // phase -> Map(kind -> bytes)
|
|
391
|
-
const activeByFeature = new Map(); // featureKey -> open task state
|
|
392
|
-
const tasks = [];
|
|
393
316
|
const newUsageBucket = () => ({
|
|
394
317
|
totals: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 },
|
|
395
318
|
ids: new Set(),
|
|
@@ -402,24 +325,6 @@ export function aggregateCost(events, opts = {}) {
|
|
|
402
325
|
if (!e || typeof e !== 'object') continue;
|
|
403
326
|
if (e.feature) features.add(e.feature);
|
|
404
327
|
|
|
405
|
-
if (e.event === 'TaskStart' && e.task) {
|
|
406
|
-
activeByFeature.set(featureKey(e), newTaskState(e.feature, e.task, e.ts));
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (e.event === 'TaskEnd' && e.task) {
|
|
411
|
-
const key = featureKey(e);
|
|
412
|
-
const state = activeByFeature.get(key);
|
|
413
|
-
if (state && state.task === e.task) {
|
|
414
|
-
state.endTs = e.ts;
|
|
415
|
-
state.status = e.status ?? null;
|
|
416
|
-
state.open = false;
|
|
417
|
-
activeByFeature.delete(key);
|
|
418
|
-
tasks.push(finalizeTask(state));
|
|
419
|
-
}
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
328
|
if (e.event === 'SessionUsage' || e.event === 'AgentUsage') {
|
|
424
329
|
const bucket = e.event === 'SessionUsage' ? usage.session : usage.agent;
|
|
425
330
|
// Only a delta can be summed: the top-level numbers are the transcript's
|
|
@@ -437,16 +342,11 @@ export function aggregateCost(events, opts = {}) {
|
|
|
437
342
|
}
|
|
438
343
|
|
|
439
344
|
if (e.tool) toolCounts.set(e.tool, (toolCounts.get(e.tool) || 0) + 1);
|
|
440
|
-
const active = activeByFeature.get(featureKey(e));
|
|
441
|
-
if (active && e.tool) {
|
|
442
|
-
active.tools.set(e.tool, (active.tools.get(e.tool) || 0) + 1);
|
|
443
|
-
}
|
|
444
345
|
|
|
445
346
|
const read = ingestedBytes(e);
|
|
446
347
|
if (read !== null) {
|
|
447
348
|
const kind = e.kind || 'unknown';
|
|
448
349
|
bytesByKindGlobal.set(kind, (bytesByKindGlobal.get(kind) || 0) + read);
|
|
449
|
-
if (active) active.bytesByKind.set(kind, (active.bytesByKind.get(kind) || 0) + read);
|
|
450
350
|
if (e.phase) {
|
|
451
351
|
if (!phaseBytes.has(e.phase)) phaseBytes.set(e.phase, new Map());
|
|
452
352
|
const m = phaseBytes.get(e.phase);
|
|
@@ -459,18 +359,9 @@ export function aggregateCost(events, opts = {}) {
|
|
|
459
359
|
const chars = hasPromptChars ? e.promptChars : e.prompt.length;
|
|
460
360
|
dispatches.count += 1;
|
|
461
361
|
dispatches.promptChars += chars;
|
|
462
|
-
if (active) {
|
|
463
|
-
active.dispatches.count += 1;
|
|
464
|
-
active.dispatches.promptChars += chars;
|
|
465
|
-
}
|
|
466
362
|
}
|
|
467
363
|
}
|
|
468
364
|
|
|
469
|
-
// Tasks still open when the stream ends (no TaskEnd observed yet) are
|
|
470
|
-
// reported, not dropped — see the "open" contract in the docstring above.
|
|
471
|
-
for (const state of activeByFeature.values()) tasks.push(finalizeTask(state));
|
|
472
|
-
tasks.sort((a, b) => (a.startTs || '').localeCompare(b.startTs || ''));
|
|
473
|
-
|
|
474
365
|
const phases = [...phaseBytes.entries()]
|
|
475
366
|
.map(([phase, kindMap]) => ({
|
|
476
367
|
phase,
|
|
@@ -479,12 +370,9 @@ export function aggregateCost(events, opts = {}) {
|
|
|
479
370
|
}))
|
|
480
371
|
.sort((a, b) => a.phase.localeCompare(b.phase));
|
|
481
372
|
|
|
482
|
-
const taskDurationSeconds = tasks.reduce((sum, t) => sum + (t.durationSeconds || 0), 0);
|
|
483
|
-
|
|
484
373
|
return {
|
|
485
374
|
total: scoped.length,
|
|
486
375
|
features: [...features],
|
|
487
|
-
taskDurationSeconds,
|
|
488
376
|
bytesByKind: mapToObj(bytesByKindGlobal),
|
|
489
377
|
bytesTotal: sumValues(bytesByKindGlobal),
|
|
490
378
|
dispatches,
|
|
@@ -496,7 +384,6 @@ export function aggregateCost(events, opts = {}) {
|
|
|
496
384
|
? { ...usage.agent.totals, agents: usage.agent.ids.size, samples: usage.agent.samples }
|
|
497
385
|
: null,
|
|
498
386
|
tools: rank(toolCounts),
|
|
499
|
-
tasks,
|
|
500
387
|
phases,
|
|
501
388
|
};
|
|
502
389
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `Verify` telemetry event: one line per `morph-spec verify` run with the
|
|
3
|
-
*
|
|
3
|
+
* per-node status and duration, the commit it covered and whether the
|
|
4
4
|
* reuse validator could run. The Bash event of the same command says who
|
|
5
5
|
* called it and how long the whole command took; this one says what it did.
|
|
6
6
|
*/
|
|
@@ -32,8 +32,6 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
|
|
|
32
32
|
return {
|
|
33
33
|
event: 'Verify',
|
|
34
34
|
feature: result.feature,
|
|
35
|
-
task: result.task ?? null,
|
|
36
|
-
scope: result.scope,
|
|
37
35
|
status: result.status,
|
|
38
36
|
sha,
|
|
39
37
|
fingerprint,
|
|
@@ -46,21 +44,21 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
|
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
/**
|
|
49
|
-
* The last complete run of the same
|
|
47
|
+
* The last complete run of the same feature's verify over the
|
|
50
48
|
* same tree, or null. A partial run (`--filter`, `--skip-*`, `--no-persist`)
|
|
51
49
|
* is not the proof a complete one replaces, so it is passed over. Only the
|
|
52
50
|
* live log is read: a repeat worth warning about is recent.
|
|
53
51
|
*
|
|
54
52
|
* @param {string} projectPath
|
|
55
|
-
* @param {{ feature: string,
|
|
53
|
+
* @param {{ feature: string, fingerprint: string|null }} run
|
|
56
54
|
* @returns {{ ts: string, status: string, sha: string|null }|null}
|
|
57
55
|
*/
|
|
58
|
-
export function findRepeatedVerify(projectPath, { feature,
|
|
56
|
+
export function findRepeatedVerify(projectPath, { feature, fingerprint }) {
|
|
59
57
|
if (!fingerprint) return null;
|
|
60
58
|
const events = readTelemetryEvents(projectPath);
|
|
61
59
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
62
60
|
const e = events[i];
|
|
63
|
-
if (e.event !== 'Verify' || e.feature !== feature
|
|
61
|
+
if (e.event !== 'Verify' || e.feature !== feature) continue;
|
|
64
62
|
if (e.persisted === false) continue;
|
|
65
63
|
return e.fingerprint === fingerprint ? { ts: e.ts, status: e.status, sha: e.sha ?? null } : null;
|
|
66
64
|
}
|
|
@@ -89,7 +89,7 @@ export function budgetIssues(measured) {
|
|
|
89
89
|
level: 'warning',
|
|
90
90
|
message:
|
|
91
91
|
`${processLines.length} de ${comments} linhas de comentário adicionadas citam processo ` +
|
|
92
|
-
`(UC, ADR,
|
|
92
|
+
`(UC, ADR, FR, C{n}, F{n}, Gate, plan.md, checks.json, mockup): ${shown.join(', ')}${more > 0 ? ` (+${more})` : ''}`,
|
|
93
93
|
file: processLines[0].file,
|
|
94
94
|
line: processLines[0].line,
|
|
95
95
|
solution:
|