@polymorphism-tech/morph-spec 9.0.0 → 9.1.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +4 -62
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +11 -67
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- 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 +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +34 -12
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +14 -96
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +28 -275
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* orchestrator and sub-agent, time per command class, identical reruns, file
|
|
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
|
-
* window compares with an earlier one.
|
|
7
|
-
*
|
|
8
|
-
* Every number degrades to null ("not measurable here") when the stream lacks
|
|
9
|
-
* the field it needs — a v1 event never counts as a zero.
|
|
10
|
-
*
|
|
11
|
-
* A rerun is the same producer command (what runs before the output filter:
|
|
12
|
-
* `dotnet test | tail -25` then `| tail -20` is one test run twice) in the same
|
|
13
|
-
* feature within 10 minutes of its previous run. `rate` counts the repeats;
|
|
14
|
-
* `burstRate` counts every run that belongs to a burst, the first included.
|
|
15
|
-
*/
|
|
1
|
+
// Every number degrades to null when the stream lacks its field: a v1 event never counts as a zero.
|
|
2
|
+
// A rerun is the same producer command (before the output filter) in the same feature within 10 minutes.
|
|
16
3
|
|
|
17
4
|
import { ingestedBytes } from './telemetry-reader.js';
|
|
18
5
|
import { producerOf } from '../../../framework/hooks/shared/command-tokens.js';
|
|
@@ -25,22 +12,13 @@ const READ_ONLY = new Set(['grep', 'rg', 'sed', 'ls', 'cat', 'find', 'head', 'ta
|
|
|
25
12
|
const TOOL_EVENTS = new Set(['PostToolUse', 'PostToolUseFailure']);
|
|
26
13
|
const NO_FEATURE = '\u0000';
|
|
27
14
|
|
|
28
|
-
|
|
29
|
-
* The command without quoted text and heredoc bodies: `morph-spec verify`
|
|
30
|
-
* inside a commit message is not a verify run.
|
|
31
|
-
*/
|
|
15
|
+
// `morph-spec verify` inside a commit message is not a verify run.
|
|
32
16
|
function executedText(command) {
|
|
33
17
|
const beforeHeredoc = command.split('<<', 1)[0];
|
|
34
18
|
return beforeHeredoc.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, (q) => q[0] + ' '.repeat(q.length - 2) + q[0]);
|
|
35
19
|
}
|
|
36
20
|
|
|
37
|
-
/**
|
|
38
|
-
* Kind (fine) and class (coarse) of a shell command. Searched anywhere in the
|
|
39
|
-
* command, most specific first, so `cd x && dotnet test` is a test.
|
|
40
|
-
*
|
|
41
|
-
* @param {string} command
|
|
42
|
-
* @returns {{ kind: string, klass: string }}
|
|
43
|
-
*/
|
|
21
|
+
/** Searched anywhere in the command, most specific first, so `cd x && dotnet test` is a test. */
|
|
44
22
|
export function classifyCommand(command) {
|
|
45
23
|
const cmd = String(command || '');
|
|
46
24
|
if (/\bmorph-spec(?:\.js)?\s+verify\s+[^\s;&|)]+/.test(executedText(cmd))) return { kind: 'verify', klass: 'verify' };
|
|
@@ -120,11 +98,8 @@ function finishCounters(c, turnsObserved) {
|
|
|
120
98
|
};
|
|
121
99
|
}
|
|
122
100
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* output filter need no exit code; the pass/fail split does, so a masked or v1
|
|
126
|
-
* exit code falls back to "after an edit or not".
|
|
127
|
-
*/
|
|
101
|
+
// Edits in between and a changed output filter need no exit code; the pass/fail split does, so a masked
|
|
102
|
+
// or v1 exit code falls back to "after an edit or not".
|
|
128
103
|
function rerunClass(prev, current, editsBetween) {
|
|
129
104
|
if (isV2(prev) && prev.outcome === 'timeout') return 'timeout';
|
|
130
105
|
if (editsBetween === 0 && prev.command !== current.command) return 'refilter';
|
|
@@ -147,11 +122,7 @@ function reuseStateOf(e) {
|
|
|
147
122
|
return { state: 'absent', reason: null };
|
|
148
123
|
}
|
|
149
124
|
|
|
150
|
-
/**
|
|
151
|
-
* @param {Object[]} events - deduplicated, time-ordered (loadTelemetry)
|
|
152
|
-
* @param {{ feature?: string, top?: number }} [opts]
|
|
153
|
-
* @returns {Object} see framework/docs/CLI.md, "cost --metrics"
|
|
154
|
-
*/
|
|
125
|
+
/** The returned shape is documented in framework/docs/CLI.md, "cost --metrics". */
|
|
155
126
|
export function computeMetrics(events, opts = {}) {
|
|
156
127
|
const top = Number.isInteger(opts.top) && opts.top > 0 ? opts.top : 10;
|
|
157
128
|
const scoped = (events || []).filter((e) => e && typeof e === 'object' && (!opts.feature || e.feature === opts.feature));
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telemetry aggregation — reads the append-only harness event stream
|
|
3
|
-
* (.morph/logs/events.jsonl, written by the telemetry-log hook) and summarizes
|
|
4
|
-
* the whole execution path: which skills ran, which personas were dispatched,
|
|
5
|
-
* which standards (knowledge base) were read, the tool mix, and the prompts.
|
|
6
|
-
*
|
|
7
|
-
* Pure aggregation (`aggregateTelemetry`) is separated from disk I/O
|
|
8
|
-
* (`loadTelemetry`) so the summary logic is unit-testable without fixtures —
|
|
9
|
-
* the same split fleet-scanner uses. CLI-side; runs in the target project cwd.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
1
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
13
2
|
import { join, basename } from 'path';
|
|
14
3
|
import { parseWorktreeList, resolvePrimaryRoot } from '../../commands/project/worktree.js';
|
|
@@ -21,11 +10,8 @@ const EVENTS_LOG = /^events(-.+)?\.jsonl$/;
|
|
|
21
10
|
// Caches a finish backup may hold; none of them contains a log.
|
|
22
11
|
const BACKUP_SKIP_DIRS = new Set(['node_modules', '.next', 'bin', 'obj', 'TestResults', 'graphify-out', '.git']);
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* and every finish backup that copied it. v2 events carry an `eventId`; a v1
|
|
27
|
-
* event is identical only to a byte-identical copy of itself.
|
|
28
|
-
*/
|
|
13
|
+
// The same line arrives through the live log, its rotated archives and every finish backup. v2 events
|
|
14
|
+
// carry an `eventId`; a v1 event is identical only to a byte-identical copy of itself.
|
|
29
15
|
function dedupeKey(event) {
|
|
30
16
|
return event.eventId ? `id:${event.eventId}` : `raw:${JSON.stringify(event)}`;
|
|
31
17
|
}
|
|
@@ -35,13 +21,7 @@ function eventTime(event) {
|
|
|
35
21
|
return Number.isNaN(t) ? 0 : t;
|
|
36
22
|
}
|
|
37
23
|
|
|
38
|
-
/**
|
|
39
|
-
* Collapse copies of the same event and restore time order across sources.
|
|
40
|
-
* The sort is stable, so events sharing a timestamp keep their write order.
|
|
41
|
-
*
|
|
42
|
-
* @param {Object[]} events
|
|
43
|
-
* @returns {Object[]}
|
|
44
|
-
*/
|
|
24
|
+
/** Stable sort, so events sharing a timestamp keep their write order. */
|
|
45
25
|
export function dedupeEvents(events) {
|
|
46
26
|
const seen = new Set();
|
|
47
27
|
const out = [];
|
|
@@ -61,9 +41,9 @@ function readJsonlFile(path, out) {
|
|
|
61
41
|
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
|
62
42
|
const trimmed = line.trim();
|
|
63
43
|
if (!trimmed) continue;
|
|
64
|
-
try { out.push(JSON.parse(trimmed)); } catch {
|
|
44
|
+
try { out.push(JSON.parse(trimmed)); } catch {}
|
|
65
45
|
}
|
|
66
|
-
} catch {
|
|
46
|
+
} catch {}
|
|
67
47
|
}
|
|
68
48
|
|
|
69
49
|
function archiveFiles(sessionsDir) {
|
|
@@ -77,15 +57,7 @@ function archiveFiles(sessionsDir) {
|
|
|
77
57
|
}
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
/**
|
|
81
|
-
* Read the event stream of a project root: the live log plus, by default, the
|
|
82
|
-
* archives SessionStart rotated into `.morph/logs/sessions/` — reading only the
|
|
83
|
-
* live file loses everything older than the last 5 MB.
|
|
84
|
-
*
|
|
85
|
-
* @param {string} [projectPath]
|
|
86
|
-
* @param {{ archives?: boolean }} [options]
|
|
87
|
-
* @returns {Object[]} deduplicated, in time order; [] when there is no log
|
|
88
|
-
*/
|
|
60
|
+
/** Live log plus, by default, the rotated archives: the live file alone loses everything past 5 MB. */
|
|
89
61
|
export function loadTelemetry(projectPath, { archives = true } = {}) {
|
|
90
62
|
const root = projectPath || process.cwd();
|
|
91
63
|
const events = [];
|
|
@@ -108,14 +80,7 @@ function collectBackupLogs(dir, depth, out) {
|
|
|
108
80
|
}
|
|
109
81
|
}
|
|
110
82
|
|
|
111
|
-
/**
|
|
112
|
-
* Event logs that `finish` saved under `<git-common-dir>/morph-spec/finish-backup/`
|
|
113
|
-
* before tearing a worktree down — once the worktree is gone, a feature built
|
|
114
|
-
* there leaves its telemetry nowhere else.
|
|
115
|
-
*
|
|
116
|
-
* @param {string} [cwd]
|
|
117
|
-
* @returns {{ path: string, events: Object[] }|null}
|
|
118
|
-
*/
|
|
83
|
+
/** Logs `finish` saved before tearing a worktree down: once it is gone, they are the feature's only telemetry. */
|
|
119
84
|
export function loadFinishBackupTelemetry(cwd = process.cwd()) {
|
|
120
85
|
const common = resolveGitCommonDir(cwd);
|
|
121
86
|
if (!common) return null;
|
|
@@ -128,19 +93,7 @@ export function loadFinishBackupTelemetry(cwd = process.cwd()) {
|
|
|
128
93
|
return { path: base, events: dedupeEvents(events) };
|
|
129
94
|
}
|
|
130
95
|
|
|
131
|
-
/**
|
|
132
|
-
* Every git worktree that has its OWN `.morph/logs/events.jsonl` — the source
|
|
133
|
-
* of `cost --all-trees` — plus a `finish-backup` entry for worktrees already
|
|
134
|
-
* torn down. Reuses the worktree enumeration of `doctor --worktrees`/`fleet`.
|
|
135
|
-
*
|
|
136
|
-
* The PRIMARY worktree is labeled `'root'`; every other entry by its directory
|
|
137
|
-
* basename (typically the feature slug). A tree that never ran the harness
|
|
138
|
-
* contributes nothing. Outside a git repo this degrades to a single root
|
|
139
|
-
* entry — never throws.
|
|
140
|
-
*
|
|
141
|
-
* @param {string} [cwd]
|
|
142
|
-
* @returns {Array<{ tree: string, path: string, events: Object[] }>}
|
|
143
|
-
*/
|
|
96
|
+
/** Labels the primary worktree `'root'`, the others by directory basename. Outside git, a single root entry. */
|
|
144
97
|
export function loadTelemetryAllTrees(cwd = process.cwd()) {
|
|
145
98
|
const root = resolvePrimaryRoot(cwd);
|
|
146
99
|
const entries = parseWorktreeList(root);
|
|
@@ -167,21 +120,6 @@ function rank(map) {
|
|
|
167
120
|
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
168
121
|
}
|
|
169
122
|
|
|
170
|
-
/**
|
|
171
|
-
* Summarize an event stream.
|
|
172
|
-
*
|
|
173
|
-
* @param {Object[]} events - raw events (from loadTelemetry)
|
|
174
|
-
* @param {{ feature?: string }} [opts] - optional feature filter
|
|
175
|
-
* @returns {{
|
|
176
|
-
* total: number,
|
|
177
|
-
* skills: Array<{name:string,count:number}>,
|
|
178
|
-
* personas: Array<{name:string,count:number}>,
|
|
179
|
-
* standards: Array<{path:string,count:number}>,
|
|
180
|
-
* tools: Array<{name:string,count:number}>,
|
|
181
|
-
* prompts: Array<{ts:string,source:'user'|'agent',feature:?string,phase:?string,text:string}>,
|
|
182
|
-
* features: string[]
|
|
183
|
-
* }}
|
|
184
|
-
*/
|
|
185
123
|
export function aggregateTelemetry(events, opts = {}) {
|
|
186
124
|
const scoped = opts.feature
|
|
187
125
|
? (events || []).filter(e => e.feature === opts.feature)
|
|
@@ -226,21 +164,8 @@ export function aggregateTelemetry(events, opts = {}) {
|
|
|
226
164
|
};
|
|
227
165
|
}
|
|
228
166
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
// folded in: aggregateTelemetry's return shape is pinned by
|
|
232
|
-
// test/commands/telemetry.test.js, which spreads it wholesale into --json
|
|
233
|
-
// output — adding fields there would leak into that contract).
|
|
234
|
-
//
|
|
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).
|
|
238
|
-
//
|
|
239
|
-
// "Bytes re-ingested" is a DETERMINISTIC PROXY for cost, not a token count —
|
|
240
|
-
// hooks can only see what tools read off disk, never the model's actual
|
|
241
|
-
// token usage. The `cost` command surfaces that caveat; this module only
|
|
242
|
-
// computes the numbers.
|
|
243
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
167
|
+
// Kept apart from aggregateTelemetry: its return shape is spread into `telemetry --json`, so new fields
|
|
168
|
+
// there would leak into that contract. Bytes re-ingested is a proxy for cost, never a token count.
|
|
244
169
|
|
|
245
170
|
function mapToObj(map) {
|
|
246
171
|
const out = {};
|
|
@@ -254,45 +179,8 @@ function sumValues(map) {
|
|
|
254
179
|
return total;
|
|
255
180
|
}
|
|
256
181
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
* `kind` (global and per phase) and sub-agent dispatch counts.
|
|
260
|
-
*
|
|
261
|
-
* Byte attribution prefers `bytesRead` (the line window a Read put in context)
|
|
262
|
-
* and falls back to `bytes` (the file size — all v1 events have) — keyed off
|
|
263
|
-
* the number alone, not the tool.
|
|
264
|
-
*
|
|
265
|
-
* Dispatch prompt size (Furo 3 — contract with the hooks-side telemetry-log
|
|
266
|
-
* rewrite): a dispatch event's `prompt` field is now TRUNCATED to 2k chars on
|
|
267
|
-
* write, with the original length carried separately as `promptChars`. When
|
|
268
|
-
* `promptChars` (a number) is present it is preferred; a legacy stream that
|
|
269
|
-
* only ever had the untruncated `prompt` string falls back to
|
|
270
|
-
* `prompt.length`, unchanged from before — every existing byte count stays
|
|
271
|
-
* exact against old logs.
|
|
272
|
-
*
|
|
273
|
-
* SessionUsage (main session) and AgentUsage (each sub-agent) are real token
|
|
274
|
-
* counts, not the bytes proxy, and never touch toolCounts/bytesByKind/
|
|
275
|
-
* dispatches. Only their `delta` is summed — the top-level numbers are a
|
|
276
|
-
* running total repeated on every sample — so a v1 sample (no `delta`) is
|
|
277
|
-
* counted in `sessionUsageLegacySamples` and never added in. Like any event
|
|
278
|
-
* they obey the `--feature` filter. `sessionUsage`/`agentUsage` are `null`
|
|
279
|
-
* when nothing summable was observed, never a zeroed object.
|
|
280
|
-
*
|
|
281
|
-
* @param {Object[]} events - raw events (from loadTelemetry)
|
|
282
|
-
* @param {{ feature?: string }} [opts] - optional feature filter
|
|
283
|
-
* @returns {{
|
|
284
|
-
* total: number,
|
|
285
|
-
* features: string[],
|
|
286
|
-
* bytesByKind: Object<string, number>,
|
|
287
|
-
* bytesTotal: number,
|
|
288
|
-
* dispatches: {count:number, promptChars:number},
|
|
289
|
-
* sessionUsage: {inputTokens:number, outputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, sessions:number, samples:number}|null,
|
|
290
|
-
* sessionUsageLegacySamples: number,
|
|
291
|
-
* agentUsage: {inputTokens:number, outputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, agents:number, samples:number}|null,
|
|
292
|
-
* tools: Array<{name:string,count:number}>,
|
|
293
|
-
* phases: Array<{phase:string, bytesByKind:Object<string,number>, bytesTotal:number}>,
|
|
294
|
-
* }}
|
|
295
|
-
*/
|
|
182
|
+
// Prefers `promptChars` over `prompt.length`: the prompt is truncated on write. Session and agent usage sum
|
|
183
|
+
// only their `delta`, never the running total; they are `null` when nothing summable was seen.
|
|
296
184
|
const SESSION_USAGE_KEYS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreationTokens'];
|
|
297
185
|
|
|
298
186
|
/** Bytes a Read put in context: its window when known, else the file size. */
|
|
@@ -312,7 +200,7 @@ export function aggregateCost(events, opts = {}) {
|
|
|
312
200
|
const toolCounts = new Map();
|
|
313
201
|
const bytesByKindGlobal = new Map();
|
|
314
202
|
const dispatches = { count: 0, promptChars: 0 };
|
|
315
|
-
const phaseBytes = new Map();
|
|
203
|
+
const phaseBytes = new Map();
|
|
316
204
|
const newUsageBucket = () => ({
|
|
317
205
|
totals: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 },
|
|
318
206
|
ids: new Set(),
|
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `Verify` telemetry event: one line per `morph-spec verify` run with the
|
|
3
|
-
* per-node status and duration, the commit it covered and whether the
|
|
4
|
-
* reuse validator could run. The Bash event of the same command says who
|
|
5
|
-
* called it and how long the whole command took; this one says what it did.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { existsSync } from 'fs';
|
|
9
2
|
import { join } from 'path';
|
|
10
3
|
import { appendTelemetryEvent, readTelemetryEvents } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
11
4
|
|
|
12
5
|
const NODE_NAMES = ['build', 'tests', 'validators', 'e2e'];
|
|
13
6
|
|
|
14
|
-
/**
|
|
15
|
-
* @param {Object} result - runVerify() output
|
|
16
|
-
* @param {{ sha: string|null, tree: 'root'|'worktree', persisted: boolean, reason: string|null,
|
|
17
|
-
* fingerprint?: string|null, source?: 'local'|'ci' }} context
|
|
18
|
-
* @returns {Object}
|
|
19
|
-
*/
|
|
20
7
|
export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerprint = null, source = 'local' }) {
|
|
21
8
|
const nodes = {};
|
|
22
9
|
for (const name of NODE_NAMES) {
|
|
@@ -43,16 +30,7 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
|
|
|
43
30
|
};
|
|
44
31
|
}
|
|
45
32
|
|
|
46
|
-
/**
|
|
47
|
-
* The last complete run of the same feature's verify over the
|
|
48
|
-
* same tree, or null. A partial run (`--filter`, `--skip-*`, `--no-persist`)
|
|
49
|
-
* is not the proof a complete one replaces, so it is passed over. Only the
|
|
50
|
-
* live log is read: a repeat worth warning about is recent.
|
|
51
|
-
*
|
|
52
|
-
* @param {string} projectPath
|
|
53
|
-
* @param {{ feature: string, fingerprint: string|null }} run
|
|
54
|
-
* @returns {{ ts: string, status: string, sha: string|null }|null}
|
|
55
|
-
*/
|
|
33
|
+
/** A partial run (`--filter`, `--skip-*`, `--no-persist`) is not the proof a complete one replaces. */
|
|
56
34
|
export function findRepeatedVerify(projectPath, { feature, fingerprint }) {
|
|
57
35
|
if (!fingerprint) return null;
|
|
58
36
|
const events = readTelemetryEvents(projectPath);
|
|
@@ -65,13 +43,7 @@ export function findRepeatedVerify(projectPath, { feature, fingerprint }) {
|
|
|
65
43
|
return null;
|
|
66
44
|
}
|
|
67
45
|
|
|
68
|
-
|
|
69
|
-
* Appends only where a morph project already exists: this command must never
|
|
70
|
-
* be the one that creates `.morph/`.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} projectPath
|
|
73
|
-
* @param {Object} event
|
|
74
|
-
*/
|
|
46
|
+
// Appends only where a morph project exists: this command must never be the one that creates `.morph/`.
|
|
75
47
|
export function recordVerifyEvent(projectPath, event) {
|
|
76
48
|
if (!existsSync(join(projectPath, '.morph'))) return;
|
|
77
49
|
appendTelemetryEvent(event, projectPath);
|
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Code Budget Validator — o que a feature adicionou além do código.
|
|
3
|
-
*
|
|
4
|
-
* Mede, só nas linhas que esta árvore adicionou contra a branch default:
|
|
5
|
-
* (a) linhas de comentário que citam processo (task, UC, ADR, Gate...), que o
|
|
6
|
-
* standard `architecture-code-comments` manda para o commit;
|
|
7
|
-
* (b) linhas de teste ÷ linhas de produção, contra o orçamento por task do
|
|
8
|
-
* `morph-plan` (um teste por pós-condição + um de regressão por bug);
|
|
9
|
-
* (c) linhas de comentário ÷ linhas de código, porque o padrão do standard é
|
|
10
|
-
* nenhum comentário. Doc-comment (`///`, JSDoc) conta como qualquer outro.
|
|
11
|
-
*
|
|
12
|
-
* Sempre `warning`: é medida de volume, e o excesso pode estar justificado na
|
|
13
|
-
* task. Um aviso por medida, não por linha, porque cada aviso custa 1 ponto no
|
|
14
|
-
* score do validador. Qualquer falha de medição vira skip com razão.
|
|
15
|
-
*
|
|
16
|
-
* @module code-budget-validator
|
|
17
|
-
*/
|
|
1
|
+
// Sempre `warning`: é medida de volume, e o excesso pode estar justificado no plano.
|
|
18
2
|
|
|
19
3
|
import { readFileSync } from 'fs';
|
|
20
4
|
import { join } from 'path';
|
|
@@ -25,31 +9,21 @@ import { isTestFile } from '../reuse/symbol-scanner.js';
|
|
|
25
9
|
import { isGenerated, isProjectSource } from '../../../../framework/hooks/shared/source-files.js';
|
|
26
10
|
import { citesProcess, commentLines } from '../../../../framework/hooks/shared/comment-scanner.js';
|
|
27
11
|
|
|
28
|
-
/** Razão teste ÷ produção acima da qual se avisa. */
|
|
29
12
|
export const MAX_TEST_RATIO = 1.5;
|
|
30
13
|
|
|
31
14
|
/** Abaixo disto a razão não é julgada: a regressão de um bug de 2 linhas cabe aqui. */
|
|
32
15
|
export const MIN_TEST_LINES = 40;
|
|
33
16
|
|
|
34
|
-
/** Mais de uma linha de comentário a cada dez de código: os nomes não estão fazendo o trabalho. */
|
|
35
17
|
export const MAX_COMMENT_RATIO = 0.1;
|
|
36
18
|
|
|
37
19
|
/** Abaixo disto a densidade não é julgada: um comentário legítimo num arquivo pequeno já passaria da razão. */
|
|
38
20
|
export const MIN_CODE_LINES = 40;
|
|
39
21
|
|
|
40
|
-
/** Quantos locais citar no aviso de comentário antes de resumir com "(+N)". */
|
|
41
22
|
const MAX_CITED = 4;
|
|
42
23
|
|
|
43
24
|
const decimal = (n) => n.toFixed(1).replace('.', ',');
|
|
44
25
|
|
|
45
|
-
/**
|
|
46
|
-
* Puro, sem I/O.
|
|
47
|
-
*
|
|
48
|
-
* @param {Array<{path: string, content: string, added: number[]|null}>} files -
|
|
49
|
-
* `added` são as linhas adicionadas; `null` é o arquivo inteiro (novo)
|
|
50
|
-
* @returns {{commentLines: number, processLines: Array<{file: string, line: number, text: string}>,
|
|
51
|
-
* testLines: number, productionLines: number, commentsByFile: Record<string, number>}}
|
|
52
|
-
*/
|
|
26
|
+
/** `added` são as linhas adicionadas de cada arquivo; `null` é o arquivo inteiro (novo). */
|
|
53
27
|
export function measureCodeBudget(files) {
|
|
54
28
|
const measured = { commentLines: 0, processLines: [], testLines: 0, productionLines: 0, commentsByFile: {} };
|
|
55
29
|
|
|
@@ -74,10 +48,6 @@ export function measureCodeBudget(files) {
|
|
|
74
48
|
return measured;
|
|
75
49
|
}
|
|
76
50
|
|
|
77
|
-
/**
|
|
78
|
-
* @param {ReturnType<typeof measureCodeBudget>} measured
|
|
79
|
-
* @returns {Array<object>} avisos no formato do validation-runner
|
|
80
|
-
*/
|
|
81
51
|
export function budgetIssues(measured) {
|
|
82
52
|
const issues = [];
|
|
83
53
|
const { commentLines: comments, processLines, testLines, productionLines, commentsByFile = {} } = measured;
|
|
@@ -93,7 +63,7 @@ export function budgetIssues(measured) {
|
|
|
93
63
|
file: processLines[0].file,
|
|
94
64
|
line: processLines[0].line,
|
|
95
65
|
solution:
|
|
96
|
-
'A história vai na mensagem de commit e no
|
|
66
|
+
'A história vai na mensagem de commit e no plan.md; no código fica só o porquê que ' +
|
|
97
67
|
'ele não mostra (standard architecture-code-comments).',
|
|
98
68
|
});
|
|
99
69
|
}
|
|
@@ -133,16 +103,6 @@ export function budgetIssues(measured) {
|
|
|
133
103
|
return issues;
|
|
134
104
|
}
|
|
135
105
|
|
|
136
|
-
/**
|
|
137
|
-
* Validador plugado no `validation-runner` (e por ele no nó `validators` do
|
|
138
|
-
* `morph-spec verify`).
|
|
139
|
-
*
|
|
140
|
-
* @param {string} projectPath
|
|
141
|
-
* @param {string|null} [featureName] - não usado: a medida é da árvore inteira contra a default
|
|
142
|
-
* @param {object} [options]
|
|
143
|
-
* @param {Map<string, number[]|null>} [options.addedLines] - override da coleta (testes)
|
|
144
|
-
* @returns {Promise<{errors: number, warnings: number, issues: object[], _skipReason?: string}>}
|
|
145
|
-
*/
|
|
146
106
|
export async function validateCodeBudget(projectPath, featureName = null, options = {}) {
|
|
147
107
|
const result = { errors: 0, warnings: 0, issues: [] };
|
|
148
108
|
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Code Budget Validators
|
|
3
|
-
*
|
|
4
|
-
* Volume que a feature adicionou além do código: comentário que conta a
|
|
5
|
-
* história do processo e teste acima do orçamento (ver code-budget-validator.js).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
export * from './code-budget-validator.js';
|
|
9
2
|
export * from '../../../../framework/hooks/shared/comment-scanner.js';
|
|
@@ -18,7 +18,6 @@ export async function validateContracts(projectPath, featureName, options = {})
|
|
|
18
18
|
return skipped('plan.md declares no interfaces/records/enums — skipping');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// Find all .cs files in the project (excluding .morph outputs, bin, obj)
|
|
22
21
|
const csFiles = await glob('**/*.cs', {
|
|
23
22
|
cwd: projectPath,
|
|
24
23
|
ignore: ['.morph/**', '**/bin/**', '**/obj/**', '**/node_modules/**']
|
|
@@ -30,7 +29,6 @@ export async function validateContracts(projectPath, featureName, options = {})
|
|
|
30
29
|
try {
|
|
31
30
|
projectCode[file] = readFileSync(fullPath, 'utf-8');
|
|
32
31
|
} catch {
|
|
33
|
-
// Skip unreadable files
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -39,7 +37,6 @@ export async function validateContracts(projectPath, featureName, options = {})
|
|
|
39
37
|
let total = 0;
|
|
40
38
|
const interfaceResults = [];
|
|
41
39
|
|
|
42
|
-
// Validate interfaces
|
|
43
40
|
for (const iface of contracts.interfaces) {
|
|
44
41
|
total++;
|
|
45
42
|
const result = validateInterface(iface, projectCode, options);
|
|
@@ -58,7 +55,6 @@ export async function validateContracts(projectPath, featureName, options = {})
|
|
|
58
55
|
});
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
// Check for missing methods
|
|
62
58
|
for (const method of result.missingMethods || []) {
|
|
63
59
|
issues.push({
|
|
64
60
|
level: 'error',
|
|
@@ -69,7 +65,6 @@ export async function validateContracts(projectPath, featureName, options = {})
|
|
|
69
65
|
}
|
|
70
66
|
}
|
|
71
67
|
|
|
72
|
-
// Validate records/DTOs
|
|
73
68
|
for (const record of contracts.records) {
|
|
74
69
|
total++;
|
|
75
70
|
const found = Object.entries(projectCode).some(([, content]) =>
|
|
@@ -90,7 +85,6 @@ export async function validateContracts(projectPath, featureName, options = {})
|
|
|
90
85
|
}
|
|
91
86
|
}
|
|
92
87
|
|
|
93
|
-
// Validate enums
|
|
94
88
|
for (const enumDef of contracts.enums) {
|
|
95
89
|
total++;
|
|
96
90
|
const found = Object.entries(projectCode).some(([, content]) =>
|
|
@@ -138,7 +132,6 @@ function parseContracts(content) {
|
|
|
138
132
|
const records = [];
|
|
139
133
|
const enums = [];
|
|
140
134
|
|
|
141
|
-
// Extract interfaces with their methods
|
|
142
135
|
const interfaceRegex = /public\s+interface\s+(I\w+)(?:\s*:\s*[\w<>,\s]+)?\s*\{([^}]*)\}/gs;
|
|
143
136
|
let match;
|
|
144
137
|
|
|
@@ -150,18 +143,15 @@ function parseContracts(content) {
|
|
|
150
143
|
interfaces.push({ name, methods });
|
|
151
144
|
}
|
|
152
145
|
|
|
153
|
-
// Extract records/classes (DTOs)
|
|
154
146
|
const recordRegex = /public\s+(record|class|struct)\s+(\w+)/g;
|
|
155
147
|
while ((match = recordRegex.exec(content)) !== null) {
|
|
156
148
|
const type = match[1];
|
|
157
149
|
const name = match[2];
|
|
158
|
-
// Skip interface names that were already captured
|
|
159
150
|
if (!name.startsWith('I') || !interfaces.some(i => i.name === name)) {
|
|
160
151
|
records.push({ type, name });
|
|
161
152
|
}
|
|
162
153
|
}
|
|
163
154
|
|
|
164
|
-
// Extract enums
|
|
165
155
|
const enumRegex = /public\s+enum\s+(\w+)/g;
|
|
166
156
|
while ((match = enumRegex.exec(content)) !== null) {
|
|
167
157
|
enums.push({ name: match[1] });
|
|
@@ -170,9 +160,6 @@ function parseContracts(content) {
|
|
|
170
160
|
return { interfaces, records, enums };
|
|
171
161
|
}
|
|
172
162
|
|
|
173
|
-
/**
|
|
174
|
-
* Extract method signatures from an interface body
|
|
175
|
-
*/
|
|
176
163
|
function extractMethods(body) {
|
|
177
164
|
const methods = [];
|
|
178
165
|
|
|
@@ -182,7 +169,6 @@ function extractMethods(body) {
|
|
|
182
169
|
|
|
183
170
|
while ((match = methodRegex.exec(body)) !== null) {
|
|
184
171
|
const methodName = match[1];
|
|
185
|
-
// Skip property getters/setters
|
|
186
172
|
if (!['get', 'set', 'init'].includes(methodName)) {
|
|
187
173
|
methods.push(methodName);
|
|
188
174
|
}
|
|
@@ -191,9 +177,6 @@ function extractMethods(body) {
|
|
|
191
177
|
return methods;
|
|
192
178
|
}
|
|
193
179
|
|
|
194
|
-
/**
|
|
195
|
-
* Validate that an interface is implemented in project code
|
|
196
|
-
*/
|
|
197
180
|
function validateInterface(iface, projectCode, options) {
|
|
198
181
|
const result = {
|
|
199
182
|
name: iface.name,
|
|
@@ -205,7 +188,6 @@ function validateInterface(iface, projectCode, options) {
|
|
|
205
188
|
missingMethods: []
|
|
206
189
|
};
|
|
207
190
|
|
|
208
|
-
// Search for classes implementing this interface
|
|
209
191
|
const implRegex = new RegExp(`class\\s+(\\w+)\\s*(?:<[^>]*>)?\\s*:\\s*[^{]*${escapeRegex(iface.name)}`, 'g');
|
|
210
192
|
|
|
211
193
|
for (const [file, content] of Object.entries(projectCode)) {
|
|
@@ -215,7 +197,6 @@ function validateInterface(iface, projectCode, options) {
|
|
|
215
197
|
result.implementingClass = match[1];
|
|
216
198
|
result.implementingFile = file;
|
|
217
199
|
|
|
218
|
-
// Check which methods are implemented
|
|
219
200
|
for (const method of iface.methods) {
|
|
220
201
|
const methodExists = new RegExp(`(public|protected|private|internal)?\\s*(async\\s+)?\\S+\\s+${escapeRegex(method)}\\s*\\(`, 'g').test(content);
|
|
221
202
|
if (methodExists) {
|
|
@@ -225,17 +206,14 @@ function validateInterface(iface, projectCode, options) {
|
|
|
225
206
|
}
|
|
226
207
|
}
|
|
227
208
|
|
|
228
|
-
break;
|
|
209
|
+
break;
|
|
229
210
|
}
|
|
230
|
-
implRegex.lastIndex = 0;
|
|
211
|
+
implRegex.lastIndex = 0;
|
|
231
212
|
}
|
|
232
213
|
|
|
233
214
|
return result;
|
|
234
215
|
}
|
|
235
216
|
|
|
236
|
-
/**
|
|
237
|
-
* Escape special regex characters in a string
|
|
238
|
-
*/
|
|
239
217
|
function escapeRegex(str) {
|
|
240
218
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
241
219
|
}
|