@juangcarmona/lighthouse 0.4.0 → 0.6.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/README.md +9 -3
- package/assets/dotnet-template/Directory.Build.props +22 -0
- package/assets/dotnet-template/Directory.Packages.props +16 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/BackendContractAnalyzer.cs +72 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/Lighthouse.Analyzers.csproj +12 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/packages.lock.json +121 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/Handlers.cs +11 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/Lighthouse.Cqrs.csproj +5 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/packages.lock.json +6 -0
- package/assets/dotnet-template/Lighthouse.slnx +17 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/CatalogModule.cs +24 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/CreateEntry.cs +47 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/EntryStore.cs +17 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/GetEntry.cs +30 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Lighthouse.Example.Modules.Catalog.csproj +12 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/packages.lock.json +10 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Lighthouse.Example.Api.csproj +6 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Program.cs +25 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Properties/launchSettings.json +10 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/packages.lock.json +86 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Lighthouse.Example.AppHost.csproj +13 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Program.cs +3 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Properties/launchSettings.json +15 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/aspire.config.json +5 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/packages.lock.json +689 -0
- package/assets/dotnet-template/global.json +6 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/BackendContractTests.cs +102 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/Lighthouse.Analyzers.Tests.csproj +9 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/packages.lock.json +125 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/CatalogTests.cs +111 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/Lighthouse.Example.Tests.csproj +9 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/packages.lock.json +490 -0
- package/assets/guardrails/catalog.json +10 -0
- package/assets/profiles/catalog.json +48 -0
- package/assets/profiles/dotnet/README.md +32 -0
- package/assets/profiles/dotnet/SKILL.md +18 -0
- package/assets/profiles/dotnet/adoption.md +39 -0
- package/assets/profiles/dotnet/architecture.md +37 -0
- package/assets/profiles/dotnet/guardrails.md +32 -0
- package/assets/profiles/dotnet/practices.md +36 -0
- package/assets/profiles/dotnet/testing-and-development.md +29 -0
- package/assets/profiles/dotnet/use-cases.md +29 -0
- package/assets/schemas/lighthouse-manifest.schema.json +11 -7
- package/assets/schemas/profile-catalog.schema.json +63 -0
- package/package.json +6 -2
- package/src/create-dotnet.mjs +112 -0
- package/src/guardrails.mjs +17 -2
- package/src/lighthouse.mjs +156 -24
- package/src/profiles.mjs +133 -0
- package/src/verify-dotnet.mjs +85 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Backend structure and boundaries
|
|
2
|
+
|
|
3
|
+
Use the product namespace under juangcarmona. The example's namespace is `JuanGCarmona.Lighthouse.Example`; the creator replaces it with `JuanGCarmona.<Product>`. Established products retain their namespace as a documented adoption choice.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
src/
|
|
7
|
+
root/<Product>.Api/ host, HTTP policy, composition
|
|
8
|
+
root/<Product>.AppHost/ optional development orchestration
|
|
9
|
+
modules/<Product>.Modules.<Name>/
|
|
10
|
+
<Name>Module.cs public registration and route composition
|
|
11
|
+
Features/<Feature>/
|
|
12
|
+
CreateThing.cs one complete command slice
|
|
13
|
+
GetThing.cs one complete query slice
|
|
14
|
+
Domain/ when the feature needs domain behavior
|
|
15
|
+
Infrastructure/Persistence/ when there is actual persistence
|
|
16
|
+
dotnet/Lighthouse.Cqrs/ initial typed command/query contracts
|
|
17
|
+
dotnet/Lighthouse.Analyzers/ local, versioned tooling source
|
|
18
|
+
src/dotnet/tests/ analyzer, HTTP and architecture tests
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Paths describe responsibilities; create folders only when they hold real code. The distributed starter keeps its examples under `src/`; generated products start directly under `src/`.
|
|
22
|
+
|
|
23
|
+
## Dependencies
|
|
24
|
+
|
|
25
|
+
The API host composes modules. A module may depend on a sibling's separate `.Contracts` assembly, never its implementation assembly. Add a contracts project only when a consumer exists. Keep contracts small: integration DTOs, stable interfaces and events that describe a real boundary. A separate contracts assembly is a structural boundary; it does not by itself guarantee safe data exposure or correct authorization.
|
|
26
|
+
|
|
27
|
+
Within a feature, use cases coordinate domain behavior and infrastructure. A command may write; a query returns a read model and must not mutate business state. Keep the domain independent of ASP.NET Core and storage APIs. A schema/DbContext per module is recommended when data ownership warrants that cost; cross-module joins or shared tables need an explicit decision. An in-memory single-module example cannot prove those persistence boundaries.
|
|
28
|
+
|
|
29
|
+
## Composition and cross-cutting behavior
|
|
30
|
+
|
|
31
|
+
Use explicit DI registration first. Introduce scanning or dispatch only when it removes measured repetition. The example injects typed handlers directly; it makes no mediator package mandatory. When logging, validation, authorization or caching become decorators, document and test their order. Authorization belongs at the edge and at the relevant domain/data boundary; validation must execute before mutation, including invocations outside HTTP.
|
|
32
|
+
|
|
33
|
+
Transactions belong to the consistency boundary of a command. Use domain events for real domain coordination; introduce an outbox when delivery must survive process/transaction failure. Cache keys and invalidation must be tested together. These capabilities are recommendations with adoption costs, not speculative packages in the starter.
|
|
34
|
+
|
|
35
|
+
## Runtime and diagnostics
|
|
36
|
+
|
|
37
|
+
Aspire owns the optional local dependency graph and startup feedback. A deployment can use containers, a managed host or another product-approved topology. Production readiness covers configuration, identity, persistent data, migrations, health, telemetry, backup/recovery and release controls; the example implements only the smaller contract documented in its README.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# .NET guardrails and enforcement
|
|
2
|
+
|
|
3
|
+
Each product maintainer owns activation and exceptions. Lighthouse maintainers own the reference diagnostics and their tests. The distinction between implemented and recommended checks is part of the contract.
|
|
4
|
+
|
|
5
|
+
| Rule | Mechanism now | Failure | Exception / owner |
|
|
6
|
+
| --- | --- | --- | --- |
|
|
7
|
+
| LH1001: handlers stay internal sealed | Roslyn semantic rule for implementations of `JuanGCarmona.Lighthouse.Cqrs.ICommandHandler<,>` / `IQueryHandler<,>` | Error during module build | Maintainer records a narrow suppression and rationale; test the changed boundary |
|
|
8
|
+
| LH1002: sibling module access uses Contracts | Roslyn compilation rule checks assembly references in the same `.Modules.` family | Error even for an unused implementation reference | Redesign dependency; temporary scoped suppression requires owner/review date |
|
|
9
|
+
| Every implementation module activates the rules | Verifier inspects evaluated MSBuild properties and analyzer references for solution projects | Verifier fails if a named module silently omits enforcement | Product maintainer defines an explicit migration scope before adopting this full verifier |
|
|
10
|
+
| Nullable correctness and SDK analysis | `src/dotnet/Directory.Build.props`, warnings-as-errors | Compiler/analysis warning fails build | Narrow documented suppression |
|
|
11
|
+
| Formatting | `.editorconfig` and `dotnet format whitespace --verify-no-changes` | Verifier fails on whitespace drift | Product-owned formatting policy |
|
|
12
|
+
| Reproducible dependency resolution | Pinned versions, SDK feature band, committed NuGet locks, locked restore | Restore fails on drift | Reviewed dependency update regenerates locks |
|
|
13
|
+
| Known dependency vulnerabilities | NuGet audit of all dependencies; warnings-as-errors | Restore fails for reported advisories at low or above | Security owner decides explicit, expiring risk acceptance; no blanket audit disable |
|
|
14
|
+
| HTTP validation and authorization | Functional tests of 400/401/403/201/200/404 behavior | Test fails | Accepted behavior changes require corresponding product decision |
|
|
15
|
+
| Compiled module boundary | Reflection architecture test checks exported handlers and references | Test fails | Match product's actual architectural contract |
|
|
16
|
+
| Broken compilation rules really fail build | `src/packages/cli/internal/verify-dotnet.mjs` compiles deliberately invalid temporary projects | Verifier fails unless LH1001 and LH1002 reject the fixture | Fix tooling; never accept a fixture merely because a unit test passed |
|
|
17
|
+
| Public entity leaks, controller prohibition, domain-framework dependency | Documented profile rules; targeted automated rules are future work | Review finding today | Maintainer records exception; do not claim automatic enforcement |
|
|
18
|
+
| Persistent data/tenant isolation | Product-specific integration tests, recommended | No coverage in this in-memory example | Product owner and security/data owner define requirements |
|
|
19
|
+
|
|
20
|
+
## Activation
|
|
21
|
+
|
|
22
|
+
Attach the analyzer as an analyzer-only project reference, set `LighthouseModule=true` on implementation projects, and expose that property with `CompilerVisibleProperty`. Module assembly names follow `<Root>.Modules.<Name>` and separate contract assemblies end in `.Contracts`. The distributed Catalog project and build properties are the executable contract.
|
|
23
|
+
|
|
24
|
+
`LH1001` recognizes the supplied CQRS interfaces semantically, including aliases/inheritance. A different mediator/handler abstraction needs an adapter or another tested rule; class-name conventions alone are not enforcement. The rule skips compiler-generated code. `LH1002` checks compilation references for the configured naming family; it does not inspect payload semantics, reflection-based access, network calls or dependency injection at runtime. Use architecture/functional tests for those boundaries.
|
|
25
|
+
|
|
26
|
+
## Diagnostic lifecycle
|
|
27
|
+
|
|
28
|
+
New rules require positive and negative compiler fixtures, a stable diagnostic ID, useful remediation, release tracking and a failing-build rehearsal. Keep their compiler API compatible with the supported SDK. Move rules to the shipped ledger only as part of an accepted release. The current analyzers are local source tooling, not a published NuGet package.
|
|
29
|
+
|
|
30
|
+
## Exceptions
|
|
31
|
+
|
|
32
|
+
Record rule/diagnostic, affected paths, reason, owner, review date and compensating test. Suppress the smallest symbol/project scope, and remove the suppression after the migration. An architectural exception must not be disguised as generated code. CI runs the same verifier; required merge status still depends on the host's branch rules.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Backend practice catalog
|
|
2
|
+
|
|
3
|
+
This catalog makes the profile reviewable. Product code owners choose applicability; security/data owners accept changes to their boundaries. “Reference” means the example demonstrates a pattern. “Enforced” names the current tool. A recommendation becomes a gate only after a product implements and verifies it.
|
|
4
|
+
|
|
5
|
+
| Practice | Problem addressed / when to apply | Mechanism in this release |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| One deployable host, modules at real seams | Keep operations simple while preserving domain/data ownership; extra modules add coordination cost | Reference layout; boundary design is a review decision |
|
|
8
|
+
| Feature-first organization | Keep behavior changes adjacent; avoid mandatory layer-by-layer file hopping | Reference and guidance |
|
|
9
|
+
| One file per command/query | Keep route, DTOs, request, validation and handler together | Reference and review; file shape has no custom analyzer yet |
|
|
10
|
+
| CQRS without separate storage by default | Distinguish reads from writes without adding a broker/read replica prematurely | Typed handler contracts and two working slices |
|
|
11
|
+
| HTTP-independent handlers | Reuse/test application behavior without transport dependencies | Reference; dedicated dependency rule is future work |
|
|
12
|
+
| Internal sealed handlers | Prevent application implementation from becoming a public API or inheritance surface | Enforced LH1001 for supported handler interfaces |
|
|
13
|
+
| Public composition, private implementation | Make module registration explicit; expose only deliberate integration contracts | Reference; public entity-leak rule is future work |
|
|
14
|
+
| Sibling Contracts assemblies | Stop modules depending on implementation internals; add contracts only for an actual consumer | Enforced LH1002 plus compiled architecture test |
|
|
15
|
+
| Small shared kernel | Keep genuinely common technical contracts in one place without building an internal framework | Two typed handler interfaces; review additional abstractions |
|
|
16
|
+
| Validation before side effects | Reject bad commands whether invoked through HTTP or another path | Reference handler validates before mutation; HTTP tests |
|
|
17
|
+
| Explicit typed outcomes / ProblemDetails | Keep expected failures separate from unexpected exceptions and stable at the edge | Reference validation/missing-resource responses; HTTP tests |
|
|
18
|
+
| Cross-cutting pipeline with tested order | Avoid inconsistent logging/validation/cache behavior after repetition justifies decorators | Recommendation; document order and test cache invalidation if introduced |
|
|
19
|
+
| Trusted identity and authorization | Prevent callers from selecting their own owner/tenant boundary | JWT edge policy and 401/403 tests; tenant data checks require a persistent product slice |
|
|
20
|
+
| Module-owned persistence and migrations | Avoid accidental shared-table coupling; separate schema/DbContext when ownership warrants the complexity | Recommendation; example has only in-memory storage |
|
|
21
|
+
| Transaction boundaries per command | Make all-or-nothing behavior and concurrency conflicts explicit | Recommendation; database-backed tests required when introduced |
|
|
22
|
+
| Domain events with durable delivery when needed | Keep domain coordination explicit; use an outbox only when delivery must survive failure | Recommendation; no event/outbox framework in the starter |
|
|
23
|
+
| Read-model cache and tested invalidation | Reduce measured read cost without returning stale business state | Recommendation; no cache in the reference |
|
|
24
|
+
| Cancellation and explicit identifiers | Bound abandoned work and expose stable resource identity | Reference propagates CancellationToken and uses GUID-v7 identifiers |
|
|
25
|
+
| Same-origin/BFF where appropriate | Simplify browser session and API trust boundaries for products that benefit from one web origin | Optional architecture choice; no frontend/BFF in the example |
|
|
26
|
+
| Generated API contracts | Detect API/client drift while keeping generated files disposable | Recommendation; require a real consumer before adding generation |
|
|
27
|
+
| Structured logs and trace context | Diagnose a failed use case without logging sensitive payloads | Host development logging; exporter, redaction and event-ID policies remain product work |
|
|
28
|
+
| Aspire development graph | Start related services with observable readiness and coherent configuration | Runnable AppHost for the API; provision only dependencies the product uses |
|
|
29
|
+
| Layered verification by risk | Catch cheap faults early and reserve expensive journeys for valuable system behavior | Semantic analyzer, architecture and HTTP tests; engine/browser backend journeys added when needed |
|
|
30
|
+
| SDK/package pins and lockfiles | Make local and CI feedback reproducible | Locked restore, central versions and SDK feature band |
|
|
31
|
+
| Warnings, style and audit as failures | Surface compiler issues, code drift and known vulnerable dependencies before merge | SDK analysis, format verification and NuGet audit in the verifier |
|
|
32
|
+
| Scoped CI with evidence of routing | Control cost without letting path filters hide meaningful changes | Current CI runs all selected profile checks; selective routing needs its own tests |
|
|
33
|
+
| Concise agent/document routers | Help humans and agents find behavior, architecture and commands without duplicated truths | AGENTS entrypoint and profile navigation |
|
|
34
|
+
| Owned, expiring exceptions | Make deviations deliberate and reversible | Documented process; no expiry automation is claimed |
|
|
35
|
+
|
|
36
|
+
Use [architecture](architecture.md) for dependency direction, [use cases](use-cases.md) for implementation detail, and [guardrails](guardrails.md) for diagnostics, failure consequences and exception ownership.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Tests and development feedback
|
|
2
|
+
|
|
3
|
+
Run `npm run verify:dotnet` from Lighthouse or the generated backend. It performs locked restore, Release build, tests and a deliberately failing-build rehearsal. The repository CI definition runs this command as its .NET job.
|
|
4
|
+
|
|
5
|
+
| Level | Purpose | Reference / cost |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| Analyzer semantic fixtures | Reject forbidden shapes and allow legitimate code | Compiler in memory; fast |
|
|
8
|
+
| Architecture | Inspect actual exports and assembly dependencies | Compiled assemblies; fast |
|
|
9
|
+
| Functional HTTP | Verify routing, auth, validation and responses through the real ASP.NET pipeline | WebApplicationFactory; no network/services |
|
|
10
|
+
| Persistence | Prove queries, transactions, schema and isolation against the real engine | Add Testcontainers when persistence is introduced |
|
|
11
|
+
| Browser journey | Prove an actual user interaction across the system | Product UI only; slower lane |
|
|
12
|
+
|
|
13
|
+
Choose boundaries according to failure risk. Use domain tests for complex domain rules, functional tests for API/data integration, and a small number of valuable end-to-end journeys. Don't replace functional coverage with mocks or make every PR start a full deployment without evidence that the cost helps.
|
|
14
|
+
|
|
15
|
+
## Local loop
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
dotnet run --project src/dotnet/examples/src/root/Lighthouse.Example.Api
|
|
19
|
+
dotnet run --project src/dotnet/examples/src/root/Lighthouse.Example.AppHost
|
|
20
|
+
dotnet test src/dotnet/tests/Lighthouse.Example.Tests -c Release
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Run either the API or AppHost entrypoint. The AppHost starts the API and exposes Aspire's local dashboard; the example has no fake database dependency. Host telemetry/health are development feedback, not proof of a production collector, alerts or durable storage. When adding PostgreSQL or another service, provision it in the local graph, wait for readiness, expose configuration through DI, and add engine-backed tests.
|
|
24
|
+
|
|
25
|
+
Configure `Authentication__Authority` and `Authentication__Audience` with the product's trusted OIDC provider for authenticated manual requests. JWT defaults require issuer/signature/expiry validation and secure metadata. The test scheme and synthetic identity headers are confined to the test assembly. Missing credentials fail closed; the example ships no bypass, identity service or secret. `/health` is a minimal process-readiness route, not proof of data-service readiness.
|
|
26
|
+
|
|
27
|
+
Use user-secrets/environment/approved secret storage for development configuration. Define production logging, redaction, traces/metrics exporters, graceful shutdown, dependency timeouts and recovery according to the product. The in-memory store and lack of full production hardening are explicit example limits.
|
|
28
|
+
|
|
29
|
+
The AppHost pins MessagePack 2.5.303 to resolve [GHSA-qhrr-8q5h-9q3h](https://github.com/MessagePack-CSharp/MessagePack-CSharp/security/advisories/GHSA-qhrr-8q5h-9q3h) while retaining NuGet audit. Revisit the override when the upstream dependency includes the fix.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# One file per use case
|
|
2
|
+
|
|
3
|
+
Read the distributed starter's `CreateEntry.cs` and `GetEntry.cs` examples. Each file owns a concrete behavior rather than a technical layer.
|
|
4
|
+
|
|
5
|
+
| Part | Responsibility | Boundary |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| Route mapping | URL, binding, authorization policy, cancellation and HTTP outcome | Thin transport adapter |
|
|
8
|
+
| Request / response | External input and output shape | Never expose persistence entities |
|
|
9
|
+
| Command / query | Application intent and its inputs | No HttpContext or IResult |
|
|
10
|
+
| Validator | Structural/business input constraints before mutation | Callable for non-HTTP invocations |
|
|
11
|
+
| Handler | Coordinate the use case and domain/storage work | Internal sealed; returns application data/outcome |
|
|
12
|
+
|
|
13
|
+
Nest these types in the use-case class so related changes remain adjacent. Omit a request DTO where there is no body and no benefit; the query example binds its ID from the route. Keep reusable domain rules in the domain rather than duplicating them across validators.
|
|
14
|
+
|
|
15
|
+
## Completion checklist
|
|
16
|
+
|
|
17
|
+
1. Define the accepted behavior and failure examples before implementing the slice.
|
|
18
|
+
2. Map a Minimal API endpoint and apply its real authorization policy.
|
|
19
|
+
3. Resolve authenticated identity/tenant from trusted context when needed; never trust an owner ID supplied by the body.
|
|
20
|
+
4. Map to a command/query, validate and call an HTTP-independent handler.
|
|
21
|
+
5. Produce explicit success, validation, missing-resource and conflict outcomes as applicable. Hide unexpected exception details behind ProblemDetails and log context without secrets or payload dumps.
|
|
22
|
+
6. Add the cheapest useful tests: domain rules, functional outcomes, authorization failures and persistence boundaries where present.
|
|
23
|
+
7. Run build/analyzers and the required verification command; record any approved exception.
|
|
24
|
+
|
|
25
|
+
The example keeps validation and outcome records local rather than introducing a general results framework. Products can choose an established result/validation library with a pinned version. Cancellation, authorization, logging and transactions still need verification regardless of the library.
|
|
26
|
+
|
|
27
|
+
## Avoid accidental complexity
|
|
28
|
+
|
|
29
|
+
Do not create a controller/service/repository class for every endpoint, a contracts assembly per feature, or a separate read database because a diagram says CQRS. Extract shared code only after a real use case needs it. Keep generated clients/metadata disposable; validate generator inputs, deterministic output and consumer contracts rather than hand-editing generated files.
|
|
@@ -10,15 +10,19 @@
|
|
|
10
10
|
"schemaVersion": {"const": 1},
|
|
11
11
|
"baseline": {"const": "0.1.0"},
|
|
12
12
|
"product": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$"},
|
|
13
|
+
"application": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["shape", "stacks"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"shape": {"enum": ["unknown", "frontend", "backend", "full-stack", "service", "library", "tooling", "cli", "worker", "mobile"]},
|
|
19
|
+
"stacks": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
13
22
|
"profiles": {"type": "array", "items": {"enum": ["node-tooling", "dotnet-backend", "visual-theme"]}, "uniqueItems": true},
|
|
14
23
|
"integrations": {"type": "object", "additionalProperties": false, "required": ["productShape", "arcShape"], "properties": {"productShape": {"$ref": "#/$defs/pin"}, "arcShape": {"$ref": "#/$defs/pin"}}},
|
|
15
|
-
"packages": {"type": "object", "additionalProperties": false, "required": ["sdlc"], "properties": {"sdlc": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-sdlc"}, "version": {"$ref": "#/$defs/pin"}}}}},
|
|
16
|
-
"managed": {"
|
|
17
|
-
{"const": ["LIGHTHOUSE.md"]},
|
|
18
|
-
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml"]},
|
|
19
|
-
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json"]},
|
|
20
|
-
{"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json"]}
|
|
21
|
-
]},
|
|
24
|
+
"packages": {"type": "object", "additionalProperties": false, "required": ["sdlc"], "properties": {"sdlc": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-sdlc"}, "version": {"$ref": "#/$defs/pin"}}}, "ui": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-ui"}, "version": {"$ref": "#/$defs/pin"}}}}},
|
|
25
|
+
"managed": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}, "uniqueItems": true},
|
|
22
26
|
"repository": {
|
|
23
27
|
"type": "object",
|
|
24
28
|
"additionalProperties": false,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/juangcarmona/lighthouse/schemas/profile-catalog.schema.json",
|
|
4
|
+
"title": "Lighthouse profile catalog",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["$schema", "schemaVersion", "profiles"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"profiles": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": { "$ref": "#/$defs/profile" }
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"$defs": {
|
|
17
|
+
"profile": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": ["id", "owner", "purpose", "appliesTo", "surfaces", "verification"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
23
|
+
"owner": { "type": "string", "minLength": 1 },
|
|
24
|
+
"purpose": { "type": "string", "minLength": 1 },
|
|
25
|
+
"appliesTo": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["shapes", "stacks"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"shapes": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true },
|
|
31
|
+
"stacks": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"surfaces": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "uniqueItems": true },
|
|
35
|
+
"verification": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"required": ["layers"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"layers": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"items": { "$ref": "#/$defs/layer" },
|
|
43
|
+
"minItems": 1,
|
|
44
|
+
"uniqueItems": true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"layer": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"required": ["id", "level", "kind", "verification"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"id": { "enum": ["static", "unit", "architecture", "integration", "functional", "browser-e2e", "operational"] },
|
|
56
|
+
"level": { "enum": ["required", "recommended", "optional"] },
|
|
57
|
+
"kind": { "enum": ["hard", "soft"] },
|
|
58
|
+
"verification": { "enum": ["deterministic", "semantic", "human"] },
|
|
59
|
+
"command": { "type": ["string", "null"] }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juangcarmona/lighthouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lighthouse adoption and verification CLI",
|
|
6
6
|
"repository": {
|
|
@@ -21,14 +21,18 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@arcshape/cli": "0.1.3",
|
|
24
|
+
"@juangcarmona/lighthouse-ui": "0.2.0",
|
|
24
25
|
"@prodshape/cli": "0.22.0",
|
|
25
|
-
"@juangcarmona/lighthouse-sdlc": "0.
|
|
26
|
+
"@juangcarmona/lighthouse-sdlc": "0.6.0"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"src/lighthouse.mjs",
|
|
29
30
|
"src/guardrails.mjs",
|
|
30
31
|
"assets",
|
|
31
32
|
"README.md",
|
|
33
|
+
"src/profiles.mjs",
|
|
34
|
+
"src/create-dotnet.mjs",
|
|
35
|
+
"src/verify-dotnet.mjs",
|
|
32
36
|
"upgrades"
|
|
33
37
|
],
|
|
34
38
|
"bin": {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
7
|
+
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
8
|
+
const templateRoot = resolve(packageRoot, 'assets/dotnet-template');
|
|
9
|
+
const cliPackage = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf8'));
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const sdlcContract = JSON.parse(await readFile(require.resolve('@juangcarmona/lighthouse-sdlc/contract.json'), 'utf8'));
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const target = args[0] ? resolve(args[0]) : null;
|
|
14
|
+
const name = args.includes('--name') ? args[args.indexOf('--name') + 1] : null;
|
|
15
|
+
|
|
16
|
+
if (!target || !name || !/^[A-Z][A-Za-z0-9]{1,47}$/.test(name) || args.length !== 3 || args[1] !== '--name') {
|
|
17
|
+
process.stderr.write('Usage: lighthouse create-dotnet <empty-target> --name Workshop\nUse a PascalCase product identifier of 2–48 letters/digits.\n');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let existing = false;
|
|
22
|
+
try {
|
|
23
|
+
const info = await lstat(target);
|
|
24
|
+
if (!info.isDirectory() || info.isSymbolicLink() || (await readdir(target)).length > 0) throw new Error('The target must be a new directory or an empty directory.');
|
|
25
|
+
existing = true;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code !== 'ENOENT') throw error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const transform = (text) => text
|
|
31
|
+
.replaceAll('JuanGCarmona.Lighthouse.Example', `JuanGCarmona.${name}`)
|
|
32
|
+
.replaceAll('Lighthouse.Example', name)
|
|
33
|
+
.replaceAll('Lighthouse_Example', name)
|
|
34
|
+
.replaceAll('../../../../Lighthouse.Cqrs/', '../../dotnet/Lighthouse.Cqrs/')
|
|
35
|
+
.replaceAll('../../../../Lighthouse.Analyzers/', '../../dotnet/Lighthouse.Analyzers/')
|
|
36
|
+
.replaceAll('../../Lighthouse.Analyzers/', '../../../src/dotnet/Lighthouse.Analyzers/')
|
|
37
|
+
.replaceAll('../../examples/src/root/', '../../../src/root/');
|
|
38
|
+
|
|
39
|
+
async function copy(from, to) {
|
|
40
|
+
const info = await lstat(from);
|
|
41
|
+
if (info.isDirectory()) {
|
|
42
|
+
await mkdir(to, { recursive: true });
|
|
43
|
+
for (const entry of await readdir(from)) {
|
|
44
|
+
if (['bin', 'obj'].includes(entry)) continue;
|
|
45
|
+
await copy(join(from, entry), join(to, entry.replaceAll('Lighthouse.Example', name)));
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!info.isFile()) throw new Error('Starter sources must be ordinary files and directories.');
|
|
50
|
+
await mkdir(dirname(to), { recursive: true });
|
|
51
|
+
await writeFile(to, transform(await readFile(from, 'utf8')));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stage = await mkdtemp(join(dirname(target), '.lighthouse-create-dotnet-'));
|
|
55
|
+
try {
|
|
56
|
+
for (const [from, to] of [
|
|
57
|
+
['Lighthouse.Analyzers', 'src/dotnet/Lighthouse.Analyzers'],
|
|
58
|
+
['Lighthouse.Cqrs', 'src/dotnet/Lighthouse.Cqrs'],
|
|
59
|
+
['examples/src', 'src'],
|
|
60
|
+
['tests', 'tests/dotnet'],
|
|
61
|
+
['Directory.Build.props', 'Directory.Build.props'],
|
|
62
|
+
['Directory.Packages.props', 'Directory.Packages.props'],
|
|
63
|
+
['global.json', 'global.json'],
|
|
64
|
+
['Lighthouse.slnx', `${name}.slnx`],
|
|
65
|
+
]) await copy(resolve(templateRoot, from), resolve(stage, to));
|
|
66
|
+
|
|
67
|
+
const solutionFile = resolve(stage, `${name}.slnx`);
|
|
68
|
+
const solutionText = (await readFile(solutionFile, 'utf8'))
|
|
69
|
+
.replaceAll('examples/src/', 'src/')
|
|
70
|
+
.replaceAll('Lighthouse.Analyzers/', 'src/dotnet/Lighthouse.Analyzers/')
|
|
71
|
+
.replaceAll('Lighthouse.Cqrs/', 'src/dotnet/Lighthouse.Cqrs/')
|
|
72
|
+
.replaceAll('tests/', 'tests/dotnet/');
|
|
73
|
+
const projects = [...solutionText.matchAll(/<Project Path="[^"]+"\s*\/>/g)].map((match) => match[0]);
|
|
74
|
+
await writeFile(solutionFile, `<Solution>\n${projects.map((project) => ` ${project}`).join('\n')}\n</Solution>\n`);
|
|
75
|
+
|
|
76
|
+
await copy(resolve(packageRoot, 'src/lighthouse.mjs'), resolve(stage, 'scripts/lighthouse.mjs'));
|
|
77
|
+
await copy(resolve(packageRoot, 'src/guardrails.mjs'), resolve(stage, 'scripts/guardrails.mjs'));
|
|
78
|
+
await copy(resolve(packageRoot, 'src/profiles.mjs'), resolve(stage, 'scripts/profiles.mjs'));
|
|
79
|
+
await copy(resolve(packageRoot, 'src/verify-dotnet.mjs'), resolve(stage, 'scripts/verify-dotnet.mjs'));
|
|
80
|
+
await copy(resolve(packageRoot, 'assets/guardrails'), resolve(stage, 'assets/guardrails'));
|
|
81
|
+
await copy(resolve(packageRoot, 'assets/profiles'), resolve(stage, 'assets/profiles'));
|
|
82
|
+
await copy(resolve(packageRoot, 'assets/schemas'), resolve(stage, 'schemas'));
|
|
83
|
+
|
|
84
|
+
await writeFile(resolve(stage, 'lighthouse.json'), `${JSON.stringify({
|
|
85
|
+
$schema: './schemas/lighthouse-manifest.schema.json', schemaVersion: 1, baseline: '0.1.0',
|
|
86
|
+
product: name, application: { shape: 'backend', stacks: ['dotnet'] }, profiles: ['dotnet-backend'],
|
|
87
|
+
integrations: { productShape: null, arcShape: null },
|
|
88
|
+
packages: { sdlc: { name: '@juangcarmona/lighthouse-sdlc', version: sdlcContract.version } },
|
|
89
|
+
managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', '.lighthouse/upgrade.json', 'docs/profiles/dotnet/README.md', 'docs/profiles/dotnet/adoption.md', 'docs/profiles/dotnet/architecture.md', 'docs/profiles/dotnet/guardrails.md', 'docs/profiles/dotnet/practices.md', 'docs/profiles/dotnet/testing-and-development.md', 'docs/profiles/dotnet/use-cases.md', '.agents/skills/lighthouse-dotnet/SKILL.md'],
|
|
90
|
+
repository: {
|
|
91
|
+
layout: ['src', 'docs', 'infra', 'scripts'], publicSite: { enabled: true },
|
|
92
|
+
conventions: { commits: 'conventional', branchPattern: '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$' },
|
|
93
|
+
},
|
|
94
|
+
}, null, 2)}\n`);
|
|
95
|
+
await writeFile(resolve(stage, 'package.json'), `${JSON.stringify({
|
|
96
|
+
name: `@juangcarmona/${name.toLowerCase()}`, private: true, type: 'module', engines: { node: '>=22' },
|
|
97
|
+
scripts: { verify: 'node scripts/lighthouse.mjs verify && node scripts/verify-dotnet.mjs' },
|
|
98
|
+
}, null, 2)}\n`);
|
|
99
|
+
await writeFile(resolve(stage, 'README.md'), `# ${name}\n\nBackend seeded from Lighthouse ${cliPackage.version}. Application and tooling sources are product-owned after creation.\n\nRun dotnet restore ${name}.slnx once to create lockfiles, then npm run verify. Commit lockfiles.\n\nStart dotnet run --project src/root/${name}.Api or the optional src/root/${name}.AppHost. Configure authentication using a trusted OIDC provider for manual API access. Test identities exist only in tests.\n\nThe sample catalog uses disposable in-memory storage. Add real persistence, data isolation, telemetry and deployment policy according to product requirements. Keep each command/query's endpoint, DTOs, validation and internal sealed handler together.\n`);
|
|
100
|
+
await writeFile(resolve(stage, 'AGENTS.md'), `# ${name} engineering guidance\n\nUse the Lighthouse .NET profile from the pinned baseline revision. Keep Minimal API commands/queries as complete single-file slices. Handlers remain internal sealed and independent of HTTP. Modules use sibling Contracts assemblies. Run npm run verify.\n\nApplication and tooling source is product-owned. Do not hand-edit LIGHTHOUSE.md. Record narrow, expiring exceptions for suppressed diagnostics.\n`);
|
|
101
|
+
await mkdir(resolve(stage, '.github/workflows'), { recursive: true });
|
|
102
|
+
await writeFile(resolve(stage, '.github/workflows/ci.yml'), `name: Backend verification\non: [push, pull_request]\npermissions:\n contents: read\njobs:\n dotnet:\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: '22'\n - uses: actions/setup-dotnet@v4\n with:\n global-json-file: global.json\n - run: npm run verify\n`);
|
|
103
|
+
|
|
104
|
+
const initialized = (await import('node:child_process')).spawnSync(process.execPath, [resolve(stage, 'scripts/lighthouse.mjs'), 'init'], { cwd: stage, encoding: 'utf8' });
|
|
105
|
+
if (initialized.status !== 0) throw new Error(initialized.stderr || 'Projection generation failed.');
|
|
106
|
+
if (existing) await rmdir(target);
|
|
107
|
+
await rename(stage, target);
|
|
108
|
+
process.stdout.write(`Created ${name} in ${target}. Run dotnet restore ${name}.slnx, then npm run verify.\n`);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
await rm(stage, { recursive: true, force: true });
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
package/src/guardrails.mjs
CHANGED
|
@@ -7,6 +7,7 @@ const allowedLevels = new Set(['required', 'recommended', 'optional']);
|
|
|
7
7
|
const allowedKinds = new Set(['hard', 'soft']);
|
|
8
8
|
const allowedVerification = new Set(['deterministic', 'semantic', 'human']);
|
|
9
9
|
const allowedStatuses = new Set(['passed', 'failed', 'drift', 'unavailable', 'unconfigured', 'pending-review', 'not-applicable']);
|
|
10
|
+
const requiredVisualTokens = ['--lh-canvas', '--lh-surface', '--lh-text', '--lh-action', '--lh-border', '--lh-focus'];
|
|
10
11
|
|
|
11
12
|
function descriptorProblems(descriptor) {
|
|
12
13
|
const problems = [];
|
|
@@ -102,16 +103,30 @@ async function runDeterministicCheck(descriptor, context) {
|
|
|
102
103
|
const problems = context.validateGitConventions(context.repository);
|
|
103
104
|
return result(descriptor, problems.length ? 'failed' : 'passed', [{ value: context.branch ?? '' }, { pattern: context.repository.conventions.branchPattern }], problems);
|
|
104
105
|
}
|
|
106
|
+
if (descriptor.check === 'ui-design') {
|
|
107
|
+
const expected = ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'];
|
|
108
|
+
const missing = [];
|
|
109
|
+
const drifted = [];
|
|
110
|
+
for (const path of expected) {
|
|
111
|
+
const target = context.resolve(path);
|
|
112
|
+
if (!context.managedFiles[path] || !(await context.exists(target))) missing.push(path);
|
|
113
|
+
else if (await context.readFile(target, 'utf8') !== context.managedFiles[path]) drifted.push(`${path} has drifted; regenerate it from the visual-theme profile.`);
|
|
114
|
+
}
|
|
115
|
+
const tokenProblems = requiredVisualTokens.filter((token) => !context.uiTokens?.includes(token)).map((token) => `${token} is missing from the installed Lighthouse UI token source.`);
|
|
116
|
+
const problems = [...missing.map((path) => `${path} is missing; run lighthouse reconcile after reviewing the visual-theme profile plan.`), ...drifted, ...tokenProblems];
|
|
117
|
+
const status = drifted.length ? 'drift' : problems.length ? 'failed' : 'passed';
|
|
118
|
+
return result(descriptor, status, [...expected.map((path) => ({ path })), ...requiredVisualTokens.map((token) => ({ token }))], problems);
|
|
119
|
+
}
|
|
105
120
|
return result(descriptor, 'unavailable', [], [`No deterministic checker is registered for ${descriptor.id}.`]);
|
|
106
121
|
}
|
|
107
122
|
|
|
108
|
-
export async function evaluateGuardrails({ catalog, manifest, manifestProblems, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch }) {
|
|
123
|
+
export async function evaluateGuardrails({ catalog, manifest, manifestProblems, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch, uiTokens }) {
|
|
109
124
|
if (manifestProblems.length) {
|
|
110
125
|
const descriptor = catalog.guardrails.find((item) => item.check === 'manifest') ?? catalog.guardrails[0];
|
|
111
126
|
const failed = result(descriptor, 'failed', [{ path: 'lighthouse.json' }], manifestProblems.map((problem) => `Invalid lighthouse.json: ${problem}`));
|
|
112
127
|
return report([failed]);
|
|
113
128
|
}
|
|
114
|
-
const context = { manifest, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch };
|
|
129
|
+
const context = { manifest, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch, uiTokens };
|
|
115
130
|
const results = [];
|
|
116
131
|
for (const descriptor of catalog.guardrails) {
|
|
117
132
|
if (!appliesTo(descriptor, manifest)) {
|