@polymorphism-tech/morph-spec 8.37.1 → 8.40.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 +37 -13
- package/README.md +2 -2
- package/bin/morph-spec.js +75 -49
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -5
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +96 -36
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/mcp/registry.json +33 -2
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +10 -4
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -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 +40 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +44 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +73 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +39 -13
- package/framework/standards/STANDARDS.json +189 -14
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +100 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/feature/decisions.md +124 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +85 -15
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +30 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +63 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +4 -1
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +47 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +31 -22
- package/src/utils/agents-installer.js +338 -24
- package/src/utils/banner.js +1 -1
- package/src/utils/hooks-installer.js +75 -15
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -58,17 +58,11 @@ public sealed class TextToSpeechService
|
|
|
58
58
|
|
|
59
59
|
public TextToSpeechService(IConfiguration config)
|
|
60
60
|
{
|
|
61
|
-
// tts-1: mais rápido, menor qualidade. tts-1-hd: mais lento, maior qualidade.
|
|
62
61
|
var model = config["TTS:Model"] ?? "tts-1";
|
|
63
62
|
var apiKey = config["OpenAI:ApiKey"]!;
|
|
64
63
|
_client = new AudioClient(model, apiKey);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
/// <summary>
|
|
68
|
-
/// Converte texto em áudio e retorna bytes MP3.
|
|
69
|
-
/// Cachear o resultado é altamente recomendado para textos repetitivos — gerar o mesmo texto
|
|
70
|
-
/// duas vezes é desperdício de custo e latência.
|
|
71
|
-
/// </summary>
|
|
72
66
|
public async Task<BinaryData> SynthesizeAsync(
|
|
73
67
|
string text,
|
|
74
68
|
GeneratedSpeechVoice? voice = null,
|
|
@@ -91,14 +85,13 @@ public sealed class TextToSpeechService
|
|
|
91
85
|
}
|
|
92
86
|
```
|
|
93
87
|
|
|
94
|
-
**Registro DI
|
|
88
|
+
**Registro DI** (`Program.cs`):
|
|
95
89
|
|
|
96
90
|
```csharp
|
|
97
|
-
// Program.cs
|
|
98
91
|
builder.Services.AddSingleton<TextToSpeechService>();
|
|
99
92
|
```
|
|
100
93
|
|
|
101
|
-
**Servindo como download ou stream
|
|
94
|
+
**Servindo como download ou stream** — verifique o cache antes de gerar (ver Anti-patterns):
|
|
102
95
|
|
|
103
96
|
```csharp
|
|
104
97
|
public class AudioController(TextToSpeechService tts) : ControllerBase
|
|
@@ -106,14 +99,12 @@ public class AudioController(TextToSpeechService tts) : ControllerBase
|
|
|
106
99
|
[HttpPost("synthesize")]
|
|
107
100
|
public async Task<IActionResult> Synthesize([FromBody] SynthesizeRequest req)
|
|
108
101
|
{
|
|
109
|
-
// Verificar cache antes de gerar (ver Anti-patterns)
|
|
110
102
|
BinaryData audio = await tts.SynthesizeAsync(
|
|
111
103
|
text: req.Text,
|
|
112
104
|
voice: GeneratedSpeechVoice.Nova,
|
|
113
105
|
format: GeneratedSpeechFormat.Mp3,
|
|
114
106
|
speedRatio: 1.1f);
|
|
115
107
|
|
|
116
|
-
// the storage layer may assign the final name; use a timestamp-based name here
|
|
117
108
|
var fileName = $"tts-{DateTime.UtcNow:yyyyMMddHHmmss}.mp3";
|
|
118
109
|
return File(audio.ToStream(), "audio/mpeg", fileName);
|
|
119
110
|
}
|
|
@@ -31,7 +31,6 @@ Vision em MAF usa `ChatMessage` multimodal — a mesma abstração `IChatClient`
|
|
|
31
31
|
```csharp
|
|
32
32
|
using Microsoft.Extensions.AI;
|
|
33
33
|
|
|
34
|
-
// Constrói um ChatMessage com texto + imagem
|
|
35
34
|
ChatMessage message = new(ChatRole.User, [
|
|
36
35
|
new TextContent("O que aparece nesta imagem?"),
|
|
37
36
|
new UriContent(new Uri("https://example.com/foto.jpg"), "image/jpeg")
|
|
@@ -59,7 +58,7 @@ O `IChatClient` (e portanto `AIAgent`) aceita `IEnumerable<ChatMessage>` — voc
|
|
|
59
58
|
|
|
60
59
|
## Exemplo C# completo
|
|
61
60
|
|
|
62
|
-
Agente de visão que recebe URL de imagem e retorna descrição. Para extração de dados tipados de um documento, combine com `RunAsync<T>` (
|
|
61
|
+
Agente de visão que recebe URL de imagem e retorna descrição. Três entradas: URL pública, arquivo local via `DataContent.LoadFromAsync` (o padrão canônico MAF, preferido sempre que houver o caminho do arquivo) e bytes já em memória (ex: upload de usuário já lido). Para extração de dados tipados de um documento, combine com `RunAsync<T>` em vez de `RunAsync` — `AgentResponse<InvoiceData> r = await _agent.RunAsync<InvoiceData>(message);` e `return r.Result;` —, com o padrão completo em `ai-agents-structured-output`.
|
|
63
62
|
|
|
64
63
|
```csharp
|
|
65
64
|
using Microsoft.Agents.AI;
|
|
@@ -84,7 +83,6 @@ public sealed class VisionAgent
|
|
|
84
83
|
});
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
/// <summary>Analisa uma imagem por URL pública e retorna descrição em texto.</summary>
|
|
88
86
|
public async Task<string> AnalyzeImageUrlAsync(Uri imageUri, string question, CancellationToken ct = default)
|
|
89
87
|
{
|
|
90
88
|
ChatMessage message = new(ChatRole.User, [
|
|
@@ -96,14 +94,6 @@ public sealed class VisionAgent
|
|
|
96
94
|
return response.Text;
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
/// <summary>
|
|
100
|
-
/// Analisa imagem a partir de um arquivo local ou caminho de upload.
|
|
101
|
-
/// Usa DataContent.LoadFromAsync — padrão canônico MAF para carregar imagem de arquivo.
|
|
102
|
-
/// Para extrair dados tipados, use RunAsync<T> em vez de RunAsync:
|
|
103
|
-
/// AgentResponse<InvoiceData> r = await _agent.RunAsync<InvoiceData>(message);
|
|
104
|
-
/// return r.Result;
|
|
105
|
-
/// Veja ai-agents-structured-output para o padrão completo.
|
|
106
|
-
/// </summary>
|
|
107
97
|
public async Task<string> AnalyzeImageFileAsync(string filePath, string question, CancellationToken ct = default)
|
|
108
98
|
{
|
|
109
99
|
DataContent imageContent = await DataContent.LoadFromAsync(filePath);
|
|
@@ -116,10 +106,6 @@ public sealed class VisionAgent
|
|
|
116
106
|
return response.Text;
|
|
117
107
|
}
|
|
118
108
|
|
|
119
|
-
/// <summary>
|
|
120
|
-
/// Analisa imagem a partir de bytes já em memória (ex: upload de usuário já lido).
|
|
121
|
-
/// Use AnalyzeImageFileAsync quando tiver o caminho do arquivo — é o padrão preferido.
|
|
122
|
-
/// </summary>
|
|
123
109
|
public async Task<string> AnalyzeImageBytesAsync(BinaryData imageData, string mediaType, string question, CancellationToken ct = default)
|
|
124
110
|
{
|
|
125
111
|
ChatMessage message = new(ChatRole.User, [
|
|
@@ -138,17 +124,14 @@ public sealed class VisionAgent
|
|
|
138
124
|
```csharp
|
|
139
125
|
var agent = new VisionAgent(modelRegistry);
|
|
140
126
|
|
|
141
|
-
// Por URL pública
|
|
142
127
|
string desc = await agent.AnalyzeImageUrlAsync(
|
|
143
128
|
new Uri("https://cdn.example.com/invoice-2024.jpg"),
|
|
144
129
|
"Extraia os campos: número da nota, valor total e data de emissão.");
|
|
145
130
|
|
|
146
|
-
// Por arquivo local — padrão canônico com DataContent.LoadFromAsync
|
|
147
131
|
string desc2 = await agent.AnalyzeImageFileAsync(
|
|
148
132
|
"nota-fiscal.png",
|
|
149
133
|
"Liste todos os itens e valores desta nota fiscal.");
|
|
150
134
|
|
|
151
|
-
// Por bytes em memória (ex: upload já lido de IFormFile)
|
|
152
135
|
BinaryData data = await BinaryData.FromStreamAsync(formFile.OpenReadStream());
|
|
153
136
|
string desc3 = await agent.AnalyzeImageBytesAsync(data, "image/png",
|
|
154
137
|
"Descreva o conteúdo da imagem enviada.");
|
|
@@ -113,10 +113,6 @@ Interface contract:
|
|
|
113
113
|
```csharp
|
|
114
114
|
public interface IPromptRepository
|
|
115
115
|
{
|
|
116
|
-
/// <summary>
|
|
117
|
-
/// Returns the content of the currently active prompt for the given agent key.
|
|
118
|
-
/// Throws <see cref="InvalidOperationException"/> if no active prompt exists.
|
|
119
|
-
/// </summary>
|
|
120
116
|
Task<string> GetActivePromptAsync(string agentKey, CancellationToken ct = default);
|
|
121
117
|
}
|
|
122
118
|
```
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
## Fronteira com o testing genérico
|
|
13
13
|
|
|
14
14
|
`backend-dotnet-testing` continua dono do que é de teste em .NET: xUnit, `FakeTimeProvider`,
|
|
15
|
-
isolamento entre casos, nomes de teste, organização de fixtures
|
|
15
|
+
isolamento entre casos, nomes de teste, organização de fixtures e o orçamento de teste por task.
|
|
16
|
+
**Nada disso se repete aqui.**
|
|
16
17
|
|
|
17
18
|
Este standard cobre **só o que é de agente** — e o que é de agente cabe em uma frase: o único ponto
|
|
18
19
|
em que o seu código toca um provider é um `IChatClient`. Todo o resto deste arquivo é consequência
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Comentários no código
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 0 (always load)
|
|
5
|
+
> **Keywords:** comentário, comentario, comment, docstring, jsdoc, xml doc, história da task, código autoexplicativo
|
|
6
|
+
> **Load When:** escrevendo ou revisando código em qualquer stack
|
|
7
|
+
|
|
8
|
+
**Verified against:** C#, TypeScript e JavaScript (convenção de linguagem, sem pacote externo). Last-verified: 2026-09-24.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
**O padrão é nenhum comentário.** O código se explica pelos nomes, pelos tipos e pelos testes. Um
|
|
13
|
+
comentário é relido a cada leitura do arquivo, por quem mantém e pelo agente que abre o arquivo na
|
|
14
|
+
próxima task, e ninguém o atualiza quando o código muda. Ele só entra quando diz algo que o código
|
|
15
|
+
**não consegue** dizer, e isso é raro: a maioria dos arquivos não tem nenhum.
|
|
16
|
+
|
|
17
|
+
## Antes de escrever um comentário
|
|
18
|
+
|
|
19
|
+
Tente, nesta ordem, e pare no primeiro que resolve:
|
|
20
|
+
|
|
21
|
+
| Em vez de comentar... | Faça |
|
|
22
|
+
|---|---|
|
|
23
|
+
| o que uma variável ou função é | renomeie: `retryDelayMs`, `isEligibleForRefund()` |
|
|
24
|
+
| o que um trecho faz | extraia uma função com esse nome |
|
|
25
|
+
| um número ou string mágica | extraia uma constante nomeada |
|
|
26
|
+
| um estado que não pode acontecer | expresse no tipo (enum, união, record, `required`) |
|
|
27
|
+
| o que o código garante | escreva um teste que falha quando deixar de valer |
|
|
28
|
+
|
|
29
|
+
Só sobrando algo que nenhum desses expressa, o comentário entra.
|
|
30
|
+
|
|
31
|
+
## O que merece comentário
|
|
32
|
+
|
|
33
|
+
Só o **porquê que o código não mostra**:
|
|
34
|
+
|
|
35
|
+
| Merece | Exemplo |
|
|
36
|
+
|---|---|
|
|
37
|
+
| Invariante que o tipo não expressa | "a chave é a identidade e o hash é a versão" |
|
|
38
|
+
| Ordem que importa | "invalidar o cache ANTES de publicar: o consumidor relê na hora" |
|
|
39
|
+
| Peculiaridade externa | API de terceiro que devolve 200 com erro no corpo; bug de biblioteca, com o link |
|
|
40
|
+
| Contrato de pacote publicado | `///` ou JSDoc só em API de pacote consumido **fora** deste repositório (NuGet, npm) |
|
|
41
|
+
|
|
42
|
+
## O que não merece
|
|
43
|
+
|
|
44
|
+
- **História de processo.** Task, UC, ADR, FR, Gate, avaliador, `spec.md`, mockup, "confirmado ao
|
|
45
|
+
implementar a T11", "o Gate 3 pediu". Isso vai na mensagem de commit e no `decisions.md`, onde tem
|
|
46
|
+
data e autor e não envelhece dentro do arquivo.
|
|
47
|
+
- **Repetição do código.** `// incrementa o contador` sobre `count++`, `// valida o request` sobre
|
|
48
|
+
`validator.Validate(request)`. Nome bom substitui comentário.
|
|
49
|
+
- **Doc-comment que repete a assinatura.** `/// <summary>Obtém o pedido.</summary>` sobre
|
|
50
|
+
`GetOrder()`, JSDoc com `@param id o id`. Membro de aplicação (handler, endpoint, componente,
|
|
51
|
+
serviço interno) não leva `///` nem JSDoc, mesmo sendo `public`.
|
|
52
|
+
- **Estrutura e decoração.** Cabeçalho de arquivo, banner de seção (`// ---- Handlers ----`,
|
|
53
|
+
`#region`), comentário que só rotula o bloco seguinte.
|
|
54
|
+
- **Código comentado e TODO solto.** O git guarda o código antigo; pendência vira issue, e o
|
|
55
|
+
comentário, se ficar, cita o número dela.
|
|
56
|
+
- **Estado do mundo fora do arquivo.** "Public Sans é a fonte da interface" passa a mentir na
|
|
57
|
+
primeira troca de fonte, e nada avisa.
|
|
58
|
+
- **Afirmação sobre comportamento.** "nunca retorna vazio", "ordenado por sobrenome": isso é
|
|
59
|
+
asserção de teste. A prosa não falha quando deixa de ser verdade; o teste falha.
|
|
60
|
+
|
|
61
|
+
## Tamanho
|
|
62
|
+
|
|
63
|
+
Uma linha é o normal; teto de ~3. Acima disso o comentário justifica o volume (algoritmo não
|
|
64
|
+
óbvio, armadilha de terceiro com a reprodução). Bloco longo narrando decisões é `decisions.md` com
|
|
65
|
+
outro nome. Em volume, código de aplicação fica perto de zero: mais de uma linha de comentário a
|
|
66
|
+
cada dez de código é sinal de que os nomes não estão fazendo o trabalho.
|
|
67
|
+
|
|
68
|
+
## Exemplo
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// Ruim: rotula, conta a task, repete o código e afirma comportamento que nenhum teste cobra.
|
|
72
|
+
/**
|
|
73
|
+
* Salva o documento.
|
|
74
|
+
* @param doc o documento
|
|
75
|
+
*/
|
|
76
|
+
// T8 + T11: mutação obrigatória do relatório, item c. Faz upsert e nunca duplica.
|
|
77
|
+
async function save(doc: Doc) {
|
|
78
|
+
await upsert({ key: doc.sourceId, hash: sha256(doc.body) });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Bom: sem comentário nenhum, porque o nome e a chamada já dizem tudo...
|
|
82
|
+
async function save(doc: Doc) {
|
|
83
|
+
await upsert({ key: doc.sourceId, hash: sha256(doc.body) });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ...e um comentário só quando há um porquê invisível.
|
|
87
|
+
// A chave é a identidade e o hash é a versão: hash novo na mesma chave substitui.
|
|
88
|
+
await upsert({ key: doc.sourceId, hash: sha256(doc.body) });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Na revisão
|
|
92
|
+
|
|
93
|
+
Comentário que o código dispensava é achado da rubrica `code-quality` no Gate 3. O nó `validators`
|
|
94
|
+
do `morph-spec verify` mede, nas linhas que a feature adicionou (validador `code-budget`), quantas
|
|
95
|
+
linhas de comentário citam processo e a densidade de comentário sobre o código adicionado, e avisa
|
|
96
|
+
com os números e os arquivos mais comentados.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -113,6 +113,28 @@ respectively and the test suite stays green.
|
|
|
113
113
|
|
|
114
114
|
---
|
|
115
115
|
|
|
116
|
+
## Test budget
|
|
117
|
+
|
|
118
|
+
The plan (`morph-plan`) gives each task a budget: **one test per postcondition, plus one
|
|
119
|
+
regression test per bug fixed**. Going above it takes a one-line justification in the task.
|
|
120
|
+
|
|
121
|
+
- **Table before copy.** Scenarios of the same behavior — including its edge cases (null, empty,
|
|
122
|
+
boundary) — are rows of one `[Theory]` (`[InlineData]`/`[MemberData]`), never N `[Fact]` methods
|
|
123
|
+
that differ by one value.
|
|
124
|
+
- **No test that only pins text.** An assertion that is only the presence or absence of a string in
|
|
125
|
+
UI copy, tenant config or prose proves the file's version, not behavior. Enforce it with an
|
|
126
|
+
analyzer, a lint rule or a validation at publish time, outside the suite count. The versioned
|
|
127
|
+
prompt case (at most one structural test) is `ai-agents-testing-ai`.
|
|
128
|
+
- **Shared fixture by default.** Reuse the project's `WebApplicationFactory` fixture, fakes and
|
|
129
|
+
builders. Before writing a new helper, look for the existing one (`morph-spec graph explain
|
|
130
|
+
<Name>`; fallback without a graph: `grep -rn`).
|
|
131
|
+
- **TDD where it pays:** a bug gets its regression test first (red, then the fix), and so does pure
|
|
132
|
+
logic. Integration and endpoint tests may come after the code.
|
|
133
|
+
- **A surviving mutant** of the Gate 3 mutation proof is killed by tightening an existing assertion
|
|
134
|
+
or adding a table row, not by one new test per mutant.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
116
138
|
## Anti-Patterns
|
|
117
139
|
|
|
118
140
|
| Do NOT | Do Instead |
|
|
@@ -125,16 +125,19 @@ real sign-in attempt, not after debugging a mysterious "session never sticks" bu
|
|
|
125
125
|
|
|
126
126
|
### SDK Initialization
|
|
127
127
|
|
|
128
|
+
`lib/auth/client.ts` — `createAuthClient` takes the public auth URL as a positional argument, and the
|
|
129
|
+
React hooks are re-exported from the client instance for ergonomic imports:
|
|
130
|
+
|
|
128
131
|
```typescript
|
|
129
|
-
// lib/auth/client.ts
|
|
130
132
|
'use client';
|
|
131
133
|
import { createAuthClient } from '@neondatabase/auth/next';
|
|
132
|
-
// createAuthClient takes the public auth URL as a positional argument
|
|
133
134
|
export const authClient = createAuthClient(process.env.NEXT_PUBLIC_AUTH_URL!);
|
|
134
|
-
// Re-export the React hooks from the client instance for ergonomic imports
|
|
135
135
|
export const { useSession } = authClient;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`lib/auth/server.ts`:
|
|
136
139
|
|
|
137
|
-
|
|
140
|
+
```typescript
|
|
138
141
|
import { createNeonAuth } from '@neondatabase/auth/next/server';
|
|
139
142
|
export const auth = createNeonAuth({
|
|
140
143
|
baseUrl: process.env.NEON_AUTH_BASE_URL!,
|
|
@@ -152,8 +155,9 @@ Import the Tailwind styles for the UI components in `globals.css`:
|
|
|
152
155
|
@import '@neondatabase/auth-ui/tailwind';
|
|
153
156
|
```
|
|
154
157
|
|
|
158
|
+
`app/providers.tsx`:
|
|
159
|
+
|
|
155
160
|
```tsx
|
|
156
|
-
// app/providers.tsx
|
|
157
161
|
'use client';
|
|
158
162
|
import { NeonAuthUIProvider } from '@neondatabase/auth-ui';
|
|
159
163
|
import { authClient } from '@/lib/auth/client';
|
|
@@ -169,8 +173,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
169
173
|
|
|
170
174
|
### Auth API Route
|
|
171
175
|
|
|
176
|
+
`app/api/auth/[...path]/route.ts`:
|
|
177
|
+
|
|
172
178
|
```typescript
|
|
173
|
-
// app/api/auth/[...path]/route.ts
|
|
174
179
|
import { auth } from '@/lib/auth/server';
|
|
175
180
|
|
|
176
181
|
export const { GET, POST } = auth.handler();
|
|
@@ -182,8 +187,9 @@ export const { GET, POST } = auth.handler();
|
|
|
182
187
|
|
|
183
188
|
### Sign In / Sign Up Pages
|
|
184
189
|
|
|
190
|
+
`app/auth/sign-in/page.tsx`:
|
|
191
|
+
|
|
185
192
|
```tsx
|
|
186
|
-
// app/auth/sign-in/page.tsx
|
|
187
193
|
import { AuthView } from '@neondatabase/auth-ui';
|
|
188
194
|
|
|
189
195
|
export default function SignInPage() {
|
|
@@ -193,8 +199,11 @@ export default function SignInPage() {
|
|
|
193
199
|
</div>
|
|
194
200
|
);
|
|
195
201
|
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`app/auth/sign-up/page.tsx`:
|
|
196
205
|
|
|
197
|
-
|
|
206
|
+
```tsx
|
|
198
207
|
import { AuthView } from '@neondatabase/auth-ui';
|
|
199
208
|
|
|
200
209
|
export default function SignUpPage() {
|
|
@@ -208,8 +217,9 @@ export default function SignUpPage() {
|
|
|
208
217
|
|
|
209
218
|
### Server-Side Auth Check
|
|
210
219
|
|
|
220
|
+
`app/dashboard/page.tsx`:
|
|
221
|
+
|
|
211
222
|
```typescript
|
|
212
|
-
// app/dashboard/page.tsx
|
|
213
223
|
import { auth } from '@/lib/auth/server';
|
|
214
224
|
import { redirect } from 'next/navigation';
|
|
215
225
|
|
|
@@ -228,8 +238,9 @@ export default async function DashboardPage() {
|
|
|
228
238
|
|
|
229
239
|
### Client Component with User Data
|
|
230
240
|
|
|
241
|
+
`components/UserProfile.tsx`:
|
|
242
|
+
|
|
231
243
|
```tsx
|
|
232
|
-
// components/UserProfile.tsx
|
|
233
244
|
'use client';
|
|
234
245
|
import { useSession } from '@/lib/auth/client';
|
|
235
246
|
|
|
@@ -249,14 +260,15 @@ export function UserProfile() {
|
|
|
249
260
|
|
|
250
261
|
### Route Protection (Middleware)
|
|
251
262
|
|
|
263
|
+
`middleware.ts`. The `runtime = 'nodejs'` line is **mandatory** for any `middleware.ts` that touches
|
|
264
|
+
Neon Auth: Next.js middleware runs on the Edge runtime by default, and the network call the Neon Auth
|
|
265
|
+
SDK makes to mint the session-data cookie times out inside the Edge sandbox on `next dev` — the request
|
|
266
|
+
silently bounces back to sign-in even with a fully valid session, with no error surfaced.
|
|
267
|
+
|
|
252
268
|
```typescript
|
|
253
|
-
// middleware.ts
|
|
254
269
|
import { auth } from '@/lib/auth/server';
|
|
255
270
|
|
|
256
|
-
//
|
|
257
|
-
// runtime by default, and the network call the Neon Auth SDK makes to mint the session-data cookie
|
|
258
|
-
// times out inside the Edge sandbox on `next dev` — the request silently bounces back to sign-in
|
|
259
|
-
// even with a fully valid session, with no error surfaced. Force the Node.js runtime instead.
|
|
271
|
+
// Mandatory: on the Edge runtime the SDK's session-cookie call times out and silently bounces to sign-in.
|
|
260
272
|
export const runtime = 'nodejs';
|
|
261
273
|
|
|
262
274
|
export default auth.middleware({
|
|
@@ -294,7 +306,6 @@ export default function ProtectedPage() {
|
|
|
294
306
|
## OAuth (Social Login)
|
|
295
307
|
|
|
296
308
|
```typescript
|
|
297
|
-
// Trigger OAuth sign-in
|
|
298
309
|
await authClient.signIn.social({
|
|
299
310
|
provider: 'google',
|
|
300
311
|
callbackURL: 'http://localhost:3000/auth/callback',
|
|
@@ -307,8 +318,9 @@ Configure OAuth providers in the Neon Console under Auth > Configuration.
|
|
|
307
318
|
|
|
308
319
|
## API Route Protection
|
|
309
320
|
|
|
321
|
+
`app/api/protected/route.ts`:
|
|
322
|
+
|
|
310
323
|
```typescript
|
|
311
|
-
// app/api/protected/route.ts
|
|
312
324
|
import { auth } from '@/lib/auth/server';
|
|
313
325
|
|
|
314
326
|
export const dynamic = 'force-dynamic';
|
|
@@ -348,34 +360,38 @@ backend's real prefix and the proxy's public prefix don't happen to match charac
|
|
|
348
360
|
because this is a runtime routing mismatch (404, or the proxy calling itself), it **never shows up at
|
|
349
361
|
build time**, only when a human clicks the page.
|
|
350
362
|
|
|
363
|
+
`lib/api/prefixes.ts` — `BACKEND_API_PREFIX` is used ONLY by Server Components / Route Handlers
|
|
364
|
+
calling the .NET backend directly (e.g. `https://api.example.com`; the backend's own routes have NO
|
|
365
|
+
`/api` prefix); `PROXY_API_PREFIX` is used ONLY by Client Components calling the same-origin Next.js
|
|
366
|
+
proxy, and matches `app/api/proxy/[...path]/route.ts`:
|
|
367
|
+
|
|
351
368
|
```typescript
|
|
352
|
-
|
|
353
|
-
// BACKEND_API_PREFIX: used ONLY by Server Components / Route Handlers calling the .NET backend directly.
|
|
354
|
-
export const BACKEND_API_PREFIX = process.env.API_URL; // e.g. https://api.example.com — backend's own routes have NO /api prefix
|
|
369
|
+
export const BACKEND_API_PREFIX = process.env.API_URL;
|
|
355
370
|
|
|
356
|
-
|
|
357
|
-
export const PROXY_API_PREFIX = '/api/proxy'; // matches app/api/proxy/[...path]/route.ts
|
|
371
|
+
export const PROXY_API_PREFIX = '/api/proxy';
|
|
358
372
|
```
|
|
359
373
|
|
|
374
|
+
The two call sites below — `app/(dashboard)/catalog/page.tsx`, a Server Component calling the backend
|
|
375
|
+
directly, and `features/catalog/hooks/use-items.ts`, a Client Component going through the same-origin
|
|
376
|
+
proxy — MUST resolve to the same backend route:
|
|
377
|
+
|
|
360
378
|
```typescript
|
|
361
|
-
// app/(dashboard)/catalog/page.tsx — Server Component, direct backend call
|
|
362
379
|
import { BACKEND_API_PREFIX } from '@/lib/api/prefixes';
|
|
363
380
|
|
|
364
381
|
async function getItems() {
|
|
365
|
-
// cross-reference: PROXY_API_PREFIX below MUST resolve to the same backend route
|
|
366
382
|
const res = await fetch(`${BACKEND_API_PREFIX}/catalog/items`, { next: { revalidate: 60 } });
|
|
367
383
|
return res.json();
|
|
368
384
|
}
|
|
369
385
|
```
|
|
370
386
|
|
|
387
|
+
`features/catalog/hooks/use-items.ts`:
|
|
388
|
+
|
|
371
389
|
```typescript
|
|
372
|
-
// features/catalog/hooks/use-items.ts — Client Component, via same-origin proxy
|
|
373
390
|
import { PROXY_API_PREFIX } from '@/lib/api/prefixes';
|
|
374
391
|
|
|
375
392
|
export function useItems() {
|
|
376
393
|
return useQuery({
|
|
377
394
|
queryKey: ['catalog', 'items'],
|
|
378
|
-
// cross-reference: BACKEND_API_PREFIX above MUST resolve to the same backend route
|
|
379
395
|
queryFn: async () => (await fetch(`${PROXY_API_PREFIX}/catalog/items`)).json(),
|
|
380
396
|
});
|
|
381
397
|
}
|
|
@@ -435,31 +451,30 @@ Before marking any task that adds or touches an endpoint as done:
|
|
|
435
451
|
|
|
436
452
|
### JWT Bearer Configuration
|
|
437
453
|
|
|
454
|
+
`Infrastructure/Auth/NeonAuthOptions.cs` — `BaseUrl` is the Neon Auth Base URL, e.g.
|
|
455
|
+
`https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth`:
|
|
456
|
+
|
|
438
457
|
```csharp
|
|
439
|
-
// Infrastructure/Auth/NeonAuthOptions.cs
|
|
440
458
|
namespace {ProjectName}.Infrastructure.Auth;
|
|
441
459
|
|
|
442
460
|
public sealed class NeonAuthOptions
|
|
443
461
|
{
|
|
444
462
|
public const string SectionName = "NeonAuth";
|
|
445
463
|
|
|
446
|
-
/// <summary>Neon Auth Base URL, e.g. https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth</summary>
|
|
447
464
|
public string BaseUrl { get; set; } = string.Empty;
|
|
448
465
|
}
|
|
449
466
|
```
|
|
450
467
|
|
|
468
|
+
`Infrastructure/Auth/NeonAuthJwksProvider.cs` fetches and caches Neon Auth's raw JWKS
|
|
469
|
+
(`/.well-known/jwks.json` — there is no OIDC discovery document) for 15 minutes; an unknown `kid`
|
|
470
|
+
forces one refresh, to survive key rotation without waiting out the TTL:
|
|
471
|
+
|
|
451
472
|
```csharp
|
|
452
|
-
// Infrastructure/Auth/NeonAuthJwksProvider.cs
|
|
453
473
|
namespace {ProjectName}.Infrastructure.Auth;
|
|
454
474
|
|
|
455
475
|
using Microsoft.Extensions.Options;
|
|
456
476
|
using Microsoft.IdentityModel.Tokens;
|
|
457
477
|
|
|
458
|
-
/// <summary>
|
|
459
|
-
/// Fetches and caches Neon Auth's raw JWKS (/.well-known/jwks.json — there is no OIDC discovery
|
|
460
|
-
/// document). Cached for 15 minutes; a forced refresh happens once when an unknown `kid` is seen,
|
|
461
|
-
/// to survive key rotation without waiting out the TTL.
|
|
462
|
-
/// </summary>
|
|
463
478
|
public sealed class NeonAuthJwksProvider(HttpClient httpClient, IOptions<NeonAuthOptions> options)
|
|
464
479
|
{
|
|
465
480
|
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(15);
|
|
@@ -473,7 +488,7 @@ public sealed class NeonAuthJwksProvider(HttpClient httpClient, IOptions<NeonAut
|
|
|
473
488
|
var key = keySet.Keys.FirstOrDefault(k => k.Kid == kid);
|
|
474
489
|
if (key is not null) return key;
|
|
475
490
|
|
|
476
|
-
// Unknown kid
|
|
491
|
+
// Unknown kid may be key rotation: refresh once before giving up.
|
|
477
492
|
keySet = await GetKeySetAsync(forceRefresh: true, cancellationToken);
|
|
478
493
|
return keySet.Keys.FirstOrDefault(k => k.Kid == kid);
|
|
479
494
|
}
|
|
@@ -503,8 +518,9 @@ public sealed class NeonAuthJwksProvider(HttpClient httpClient, IOptions<NeonAut
|
|
|
503
518
|
}
|
|
504
519
|
```
|
|
505
520
|
|
|
521
|
+
`Infrastructure/Auth/NeonAuthServiceExtensions.cs`:
|
|
522
|
+
|
|
506
523
|
```csharp
|
|
507
|
-
// Infrastructure/Auth/NeonAuthServiceExtensions.cs
|
|
508
524
|
namespace {ProjectName}.Infrastructure.Auth;
|
|
509
525
|
|
|
510
526
|
using System.Text;
|
|
@@ -527,9 +543,7 @@ public static class NeonAuthServiceExtensions
|
|
|
527
543
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
528
544
|
.AddJwtBearer(options =>
|
|
529
545
|
{
|
|
530
|
-
//
|
|
531
|
-
// so Authority-driven metadata refresh has nothing valid to fetch. ValidIssuer is set
|
|
532
|
-
// explicitly below instead.
|
|
546
|
+
// No options.Authority: Neon Auth serves no OIDC discovery document, so ValidIssuer is explicit.
|
|
533
547
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
534
548
|
{
|
|
535
549
|
ValidateIssuer = true,
|
|
@@ -537,14 +551,12 @@ public static class NeonAuthServiceExtensions
|
|
|
537
551
|
ValidateAudience = false,
|
|
538
552
|
ValidateLifetime = true,
|
|
539
553
|
NameClaimType = "sub",
|
|
540
|
-
//
|
|
541
|
-
// below (fetched from our own cached JWKS) — this flag doesn't gate that path.
|
|
554
|
+
// Key trust is established inside SignatureValidator below; this flag doesn't gate that path.
|
|
542
555
|
ValidateIssuerSigningKey = false,
|
|
543
556
|
};
|
|
544
557
|
});
|
|
545
558
|
|
|
546
|
-
//
|
|
547
|
-
// so this only *adds* the SignatureValidator to the TokenValidationParameters built above.
|
|
559
|
+
// Runs after AddJwtBearer's own setup, with DI available: it only adds the SignatureValidator.
|
|
548
560
|
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
|
549
561
|
.Configure<NeonAuthJwksProvider>((options, jwks) =>
|
|
550
562
|
{
|
|
@@ -561,9 +573,7 @@ public static class NeonAuthServiceExtensions
|
|
|
561
573
|
if (string.IsNullOrEmpty(kid))
|
|
562
574
|
throw new SecurityTokenInvalidSignatureException("Token is missing 'kid'.");
|
|
563
575
|
|
|
564
|
-
// Blocking
|
|
565
|
-
// I/O-free on the hot path. SignatureValidator has no async overload in this
|
|
566
|
-
// library version — only a cold-start / key-rotation miss ever awaits real I/O here.
|
|
576
|
+
// Blocking is safe: SignatureValidator has no async overload and the JWKS is cached in memory.
|
|
567
577
|
var jwk = jwks.GetKeyAsync(kid, CancellationToken.None).GetAwaiter().GetResult()
|
|
568
578
|
?? throw new SecurityTokenInvalidSignatureException($"No JWKS key found for kid '{kid}'.");
|
|
569
579
|
|
|
@@ -585,10 +595,7 @@ public static class NeonAuthServiceExtensions
|
|
|
585
595
|
if (!verifier.VerifySignature(signature))
|
|
586
596
|
throw new SecurityTokenInvalidSignatureException("Ed25519 signature verification failed.");
|
|
587
597
|
|
|
588
|
-
//
|
|
589
|
-
// uses JsonWebTokenHandler internally. Returning a
|
|
590
|
-
// System.IdentityModel.Tokens.Jwt.JwtSecurityToken here fails with IDX10506
|
|
591
|
-
// ("SignatureValidator returned a token of an unexpected type").
|
|
598
|
+
// Must be JsonWebToken: ASP.NET Core 8+ rejects a JwtSecurityToken here with IDX10506.
|
|
592
599
|
return jwt;
|
|
593
600
|
};
|
|
594
601
|
});
|
|
@@ -600,6 +607,12 @@ public static class NeonAuthServiceExtensions
|
|
|
600
607
|
}
|
|
601
608
|
```
|
|
602
609
|
|
|
610
|
+
Two details of the validator above: it returns `Microsoft.IdentityModel.JsonWebTokens.JsonWebToken`
|
|
611
|
+
because ASP.NET Core 8+ uses `JsonWebTokenHandler` internally — returning a
|
|
612
|
+
`System.IdentityModel.Tokens.Jwt.JwtSecurityToken` fails with IDX10506 ("SignatureValidator returned a
|
|
613
|
+
token of an unexpected type"); and the blocking `GetResult()` is I/O-free on the hot path, since only a
|
|
614
|
+
cold-start or key-rotation miss ever awaits real I/O.
|
|
615
|
+
|
|
603
616
|
### Protected Endpoints
|
|
604
617
|
|
|
605
618
|
```csharp
|