From 785a45d9f12d4b49c29783fc551c24700b5bd3b0 Mon Sep 17 00:00:00 2001 From: Chris Cavell Date: Fri, 18 Sep 2026 06:00:45 -0500 Subject: [PATCH] docs: validate AsiBackbone 6.0 API references --- .github/workflows/docs-validation.yml | 4 + README.md | 2 + ...knowledgment-and-continuation-workflows.md | 2 + ...ss-boundaries-and-transaction-reasoning.md | 12 +- .../governed-administrative-operation.md | 2 + .../human-acknowledgment-workflow.md | 1 + .../asibackbone-6-api-boundary.md | 144 ++++++ docs/getting-started/index.md | 3 + docs/getting-started/toc.yml | 2 + docs/labs/build-a-governed-api-operation.md | 1 + docs/samples/index.md | 2 + docs/tutorials/index.md | 2 + samples/README.md | 2 + .../validate-asibackbone-6-api-references.cs | 423 ++++++++++++++++++ 14 files changed, 597 insertions(+), 5 deletions(-) create mode 100644 docs/getting-started/asibackbone-6-api-boundary.md create mode 100644 tools/validate-asibackbone-6-api-references.cs diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml index c5de426..072f03c 100644 --- a/.github/workflows/docs-validation.yml +++ b/.github/workflows/docs-validation.yml @@ -21,6 +21,7 @@ on: - 'tools/generate-sitemap.cs' - 'tools/prepare-indexnow.cs' - 'tools/publish-x.cs' + - 'tools/validate-asibackbone-6-api-references.cs' - 'tools/validate-doc-metadata.cs' - 'tools/validate-docfx-template-baseline.cs' - '.config/dotnet-tools.json' @@ -64,6 +65,9 @@ jobs: - name: Validate DocFX template baseline run: dotnet run --file tools/validate-docfx-template-baseline.cs + - name: Validate AsiBackbone 6.0 API references + run: dotnet run --file tools/validate-asibackbone-6-api-references.cs + - name: Restore .NET tools run: dotnet tool restore diff --git a/README.md b/README.md index afc8c9c..a5f4042 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Want to understand why this boundary exists or experiment with it? | Learn the foundational governed-execution boundary | [Decision Before Execution](docs/tutorials/decision-before-execution.md) | | Route from a problem you already recognize | [Find Your Path](docs/getting-started/find-your-path.md) | | See the curriculum and prerequisites at a glance | [Learning Path Map](docs/getting-started/learning-path-map.md) | +| Copy exact AsiBackbone 6.0 API syntax | [AsiBackbone 6.0 API Boundary](docs/getting-started/asibackbone-6-api-boundary.md) | | Decide whether ASP.NET Core authorization is already enough | [When ASP.NET Core Authorization Is Enough](docs/architecture/when-aspnet-core-authorization-is-enough.md) | ## What This Architecture Looks Like in Practice @@ -140,6 +141,7 @@ AsiBackbone Learning is an educational and architectural resource. - It teaches architectural patterns; it does not certify compliance or guarantee security. - Examples do not replace application-specific security, legal, regulatory, safety, or operational review. +- Learning-owned sample types are framework-neutral teaching models, not `AsiBackbone.*` package API signatures. - Learning is not an AI model, an artificial general intelligence or artificial superintelligence implementation, or a robotics controller. - No `AsiBackbone` package is required, and no pattern is presented as universally correct. diff --git a/docs/advanced/distributed-acknowledgment-and-continuation-workflows.md b/docs/advanced/distributed-acknowledgment-and-continuation-workflows.md index 3bd56df..172c8fb 100644 --- a/docs/advanced/distributed-acknowledgment-and-continuation-workflows.md +++ b/docs/advanced/distributed-acknowledgment-and-continuation-workflows.md @@ -150,6 +150,8 @@ accounts.bulk-suspend System A evaluates a tenant administrator's request to suspend a bounded set of fictional accounts. Current policy requires acknowledgment of the operational impact. System B presents the challenge to the required responder and produces evidence of the response. System C owns the eventual account executor. The systems are separate enough that System C cannot safely assume it still has System A's original in-memory context. The challenge therefore needs durable bindings that System C can validate later. A compact challenge model might contain: +> **Illustrative API:** This local `AcknowledgmentChallenge` shape is a teaching model, not the `AsiBackbone.AspNetCore` package type. See the [AsiBackbone 6.0 API Boundary](../getting-started/asibackbone-6-api-boundary.md) before copying package-integration syntax. + ```csharp public sealed record AcknowledgmentChallenge( string ChallengeId, diff --git a/docs/aspnetcore/data-access-boundaries-and-transaction-reasoning.md b/docs/aspnetcore/data-access-boundaries-and-transaction-reasoning.md index 4861b09..c51a545 100644 --- a/docs/aspnetcore/data-access-boundaries-and-transaction-reasoning.md +++ b/docs/aspnetcore/data-access-boundaries-and-transaction-reasoning.md @@ -312,10 +312,12 @@ But if the purpose of the repository was to hide EF Core or constrain persistenc Prefer methods that expose the operation the caller needs when the repository is intended to be a real boundary: +> **Illustrative API:** The repository and receipt names in the following examples are local teaching shapes, not `AsiBackbone.*` package signatures. See the [AsiBackbone 6.0 API Boundary](../getting-started/asibackbone-6-api-boundary.md) for the exact current receipt and persistence contracts. + ```csharp Task FindForDisableAsync(...) ValueTask TryConsumeAsync(...) -Task AppendAsync(DecisionReceipt receipt, ...) +Task AppendAsync(LearningDecisionReceipt receipt, ...) ``` Do not create one method per `DbSet` operation simply to avoid naming `DbContext`. @@ -815,7 +817,7 @@ Prefer explicit code when the operation is meaningful because of the workflow: ```csharp await decisionReceiptStore.AppendAsync( - DecisionReceipt.ExecutionStarted(...), + LearningDecisionReceipt.ExecutionStarted(...), cancellationToken); ``` @@ -1235,7 +1237,7 @@ Governance decision Execution-boundary service ↓ ICapabilityUseStore -IDecisionReceiptStore +ILearningDecisionReceiptStore ↓ EF Core implementations ↓ @@ -1265,11 +1267,11 @@ public sealed class ExecutionPersistenceCoordinator { private readonly ApplicationDbContext _dbContext; private readonly EfCoreCapabilityUseStore _useStore; - private readonly EfCoreDecisionReceiptStore _auditStore; + private readonly EfCoreLearningDecisionReceiptStore _auditStore; public async Task TryStartAsync( string capabilityId, - DecisionReceipt executionStart, + LearningDecisionReceipt executionStart, CancellationToken cancellationToken) { await using var transaction = diff --git a/docs/case-studies/governed-administrative-operation.md b/docs/case-studies/governed-administrative-operation.md index 8bf7344..2bb86df 100644 --- a/docs/case-studies/governed-administrative-operation.md +++ b/docs/case-studies/governed-administrative-operation.md @@ -664,6 +664,8 @@ One correlation identifier connects the lifecycle, but different records answer Use one explicit linkage envelope across the three core evidence types—decision, grant, and execution: +> **Illustrative API:** The receipt records below are case-study models, not `AsiBackbone.*` package signatures. Compare the exact 6.0 receipt surface in the [AsiBackbone 6.0 API Boundary](../getting-started/asibackbone-6-api-boundary.md). + ```csharp public sealed record EvidenceCorrelation( string CorrelationId, diff --git a/docs/case-studies/human-acknowledgment-workflow.md b/docs/case-studies/human-acknowledgment-workflow.md index 3609de4..bd17c88 100644 --- a/docs/case-studies/human-acknowledgment-workflow.md +++ b/docs/case-studies/human-acknowledgment-workflow.md @@ -443,6 +443,7 @@ The client cannot create a privileged acknowledgment challenge simply by request A challenge should survive beyond one HTTP request or browser session when the workflow can pause. ```csharp +// Illustrative case-study model; not an AsiBackbone package API. public enum AcknowledgmentChallengeStatus { Pending, diff --git a/docs/getting-started/asibackbone-6-api-boundary.md b/docs/getting-started/asibackbone-6-api-boundary.md new file mode 100644 index 0000000..12acc72 --- /dev/null +++ b/docs/getting-started/asibackbone-6-api-boundary.md @@ -0,0 +1,144 @@ +--- +description: Distinguish Learning teaching models from the finalized AsiBackbone 6.0 API, including supported construction, endpoint markers, and removed members. +--- + +# AsiBackbone 6.0 API Boundary + +AsiBackbone Learning teaches architecture with two different kinds of code: + +1. **Learning-owned teaching models** are small, framework-neutral types compiled from this repository. They make an architectural boundary easy to observe, but they are not package API signatures. +2. **AsiBackbone 6.0 API examples** use the finalized public names and namespaces from the implementation repository's `release/6.0.0` branch. + +Keep that distinction visible when copying an example. A local teaching type named for a concept may be intentionally smaller than the similarly named framework type. + +> **API status:** Unless a section is labeled **AsiBackbone 6.0 API**, code in Learning is illustrative or belongs to a Learning sample. Follow the linked implementation source or the [5.x to 6.0 migration guide](https://github.com/AsiBackbone/AsiBackbone/blob/release/6.0.0/docs/articles/upgrade-500-to-600.md) for exact package syntax. + +## Executable Sample Package Policy + +The projects under `samples/` are executable Learning companions, not package-integration samples. They currently contain no `PackageReference` to an `AsiBackbone.*` package. Their local records, policies, evaluators, and gateways are teaching models owned by this repository. + +That is deliberate: + +- the samples remain runnable while the 6.0 package line is prepared; +- architectural invariants can be studied without adopting a framework; +- local types do not claim to reproduce every constructor, member, namespace, persistence seam, or security control in the implementation. + +If a future sample adds an `AsiBackbone.*` package reference, it must pin a released 6.x version through the sample package-management files, update the lock file, compile against that package, and identify itself as a package-integration sample. + +## Finalized Core Names + +The complete review covers 232 public type entries across ten managed packages. The authoritative inventory is the [6.0 public API naming convention](https://github.com/AsiBackbone/AsiBackbone/blob/release/6.0.0/docs/articles/public-api-naming-600.md). + +The names most often used by Learning material are: + +| Purpose | AsiBackbone 6.0 public type | +| --- | --- | +| Evaluation context contract | `AsiBackbone.Core.Constraints.IGovernanceEvaluationContext` | +| Default context | `AsiBackbone.Core.Constraints.GovernanceEvaluationContext` | +| Constraint | `AsiBackbone.Core.Constraints.IGovernanceConstraint` | +| Policy evaluator | `AsiBackbone.Core.Evaluation.IGovernancePolicyEvaluator` | +| Default evaluator | `AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator` | +| Evaluator builder | `AsiBackbone.Core.Evaluation.GovernancePolicyEvaluatorBuilder` | +| Evaluator options | `AsiBackbone.Core.Evaluation.GovernancePolicyOptions` | +| Decision policy | `AsiBackbone.Core.Evaluation.IGovernanceDecisionPolicy` | +| Decision | `AsiBackbone.Core.Decisions.GovernanceDecision` | +| Decision receipt | `AsiBackbone.Core.Audit.DecisionReceipt` | +| Receipt sink | `AsiBackbone.Core.Audit.IDecisionReceiptSink` | +| Durable audit ledger | `AsiBackbone.Core.Audit.IGovernanceAuditLedgerStore` | +| Actor context | `AsiBackbone.Core.Actors.GovernanceActorContext` | +| ASP.NET Core endpoint service | `AsiBackbone.AspNetCore.Endpoints.IEndpointGovernanceService` | +| Acknowledgment challenge service | `AsiBackbone.AspNetCore.Handshakes.IAcknowledgmentChallengeService` | + +These are implementation types. A Learning snippet that defines its own decision, receipt, context, or acknowledgment record is a teaching model unless the section explicitly says otherwise. + +## Construct an Evaluator + +The five partial evaluator constructors retained during 5.x do not exist in 6.0. Use the builder or the full-dependency constructor. + +**AsiBackbone 6.0 API:** + +```csharp +using AsiBackbone.Core.Constraints; +using AsiBackbone.Core.Evaluation; + +IGovernancePolicyEvaluator evaluator = + DefaultGovernancePolicyEvaluator + .CreateBuilder() + .AddConstraints(constraints) + .AddThreatModelContributors(threatModelContributors) + .WithDecisionPolicy(decisionPolicy) + .WithOptions(options) + .WithLogger(logger) + .Build(); +``` + +The supported direct constructor accepts the complete dependency set: + +**AsiBackbone 6.0 API:** + +```csharp +var evaluator = new DefaultGovernancePolicyEvaluator( + constraints, + threatModelContributors: null, + decisionPolicy: null, + options: null, + logger: null); +``` + +For dependency injection, prefer a factory that resolves the host's configured constraints, contributors, decision policy, `IOptions.Value`, and logger before calling the builder. Registering only the concrete evaluator type requires the container to resolve all five constructor dependencies. + +Inspect the exact implementation in [`DefaultGovernancePolicyEvaluator`](https://github.com/AsiBackbone/AsiBackbone/blob/release/6.0.0/src/AsiBackbone.Core/Evaluation/DefaultGovernancePolicyEvaluator.cs), its [`CreateBuilder` factory](https://github.com/AsiBackbone/AsiBackbone/blob/release/6.0.0/src/AsiBackbone.Core/Evaluation/DefaultGovernancePolicyEvaluator.Factory.cs), and [`GovernancePolicyEvaluatorBuilder`](https://github.com/AsiBackbone/AsiBackbone/blob/release/6.0.0/src/AsiBackbone.Core/Evaluation/GovernancePolicyEvaluatorBuilder.cs). + +## Mark Endpoint Policy Metadata + +The 5.x `RequireGovernancePolicy` route-builder methods were removed. Use `MarkGovernancePolicy`. + +**AsiBackbone 6.0 API:** + +```csharp +using AsiBackbone.AspNetCore.Endpoints; + +app.MapPost("/operations", HandleOperation) + .MarkGovernancePolicy(); + +app.MapPost("/exports", HandleExport) + .MarkGovernancePolicy(typeof(ExportPolicyMarker)); +``` + +The marker records policy metadata. It does not, by itself, resolve a policy, select constraints, or enforce execution. See the exact [`EndpointGovernanceRouteBuilderExtensions`](https://github.com/AsiBackbone/AsiBackbone/blob/release/6.0.0/src/AsiBackbone.AspNetCore/Endpoints/EndpointGovernanceRouteBuilderExtensions.cs) behavior. + +## Removed Compatibility Surface + +The upstream obsolete-member inventory found exactly seven public members whose compatibility window ended at 6.0: + +| Removed after 5.x | Supported 6.0 path | +| --- | --- | +| `DefaultAsiBackbonePolicyEvaluator(constraints, decisionPolicy = null)` | Builder with `AddConstraints` and `WithDecisionPolicy` | +| `DefaultAsiBackbonePolicyEvaluator(constraints, decisionPolicy, options)` | Builder plus `WithOptions` | +| `DefaultAsiBackbonePolicyEvaluator(constraints, decisionPolicy, options, logger)` | Builder plus `WithOptions` and `WithLogger` | +| `DefaultAsiBackbonePolicyEvaluator(constraints, threatModelContributors, decisionPolicy = null)` | Builder plus `AddThreatModelContributors` and `WithDecisionPolicy` | +| `DefaultAsiBackbonePolicyEvaluator(constraints, threatModelContributors, decisionPolicy, options)` | Builder plus contributors, policy, and options | +| `RequireGovernancePolicy(RouteHandlerBuilder)` | `MarkGovernancePolicy()`, or `MarkGovernancePolicy(typeof(TPolicy))` for a plain marker | +| `RequireGovernancePolicy(TBuilder, Type)` | `MarkGovernancePolicy(builder, policyType)` | + +The non-obsolete `RequireGovernancePolicyAttribute` remains part of the implementation. Historical 4.x and 5.x migration material may retain old names when it is clearly presented as history; current examples must not present the removed members as callable 6.0 APIs. + +## Decision Receipt Is Not Execution Proof + +`DecisionReceipt` records the policy decision outcome and reasons. Later acknowledgment, capability, gateway, emission, and execution lifecycle evidence can correlate with it, but the decision receipt does not prove that the host executed the protected operation. + +Learning samples may use smaller local receipt or lifecycle records to make that distinction visible. Compare them with the exact 6.0 [`DecisionReceipt`](https://github.com/AsiBackbone/AsiBackbone/blob/release/6.0.0/src/AsiBackbone.Core/Audit/DecisionReceipt.cs) and [`DecisionReceiptLifecycleEvent`](https://github.com/AsiBackbone/AsiBackbone/blob/release/6.0.0/src/AsiBackbone.Core/Audit/DecisionReceiptLifecycleEvent.cs) source before adopting a package integration. + +## Review Checklist + +Before publishing an API-facing Learning change: + +- label teaching code as illustrative or Learning-owned; +- label exact framework syntax as AsiBackbone 6.0 API; +- use finalized 6.0 names and namespaces; +- do not call removed evaluator constructors or route-builder methods; +- link implementation source to `release/6.0.0`, not `main` or a 5.x branch; +- pin any future `AsiBackbone.*` sample package reference to a released 6.x version and commit its lock-file update; +- preserve old names only in clearly historical migration or release material. + +The repository's API-reference validator enforces the machine-checkable parts of this boundary during documentation validation. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index e0a4d07..d4cea6f 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -20,6 +20,7 @@ Choose the shortest path that matches how you want to learn: | Solve a specific architecture problem | [**Find Your Path**](find-your-path.md) | | See the complete curriculum visually | [**Learning Path Map**](learning-path-map.md) | | Learn by running code | [**Executable Samples**](../samples/index.md) | +| Copy exact AsiBackbone 6.0 API syntax | [**AsiBackbone 6.0 API Boundary**](asibackbone-6-api-boundary.md) | | Practice by changing or challenging the design | [**Hands-On Labs**](../labs/index.md) | | Compare a simpler alternative | [**When ASP.NET Core Authorization Is Enough**](../architecture/when-aspnet-core-authorization-is-enough.md) | @@ -106,6 +107,8 @@ The [`samples/`](../samples/index.md) area contains intentionally small .NET tea They are teaching artifacts rather than production frameworks. +Their local types are not package API signatures. Use the [AsiBackbone 6.0 API Boundary](asibackbone-6-api-boundary.md) when you need exact current namespaces, evaluator construction, endpoint markers, or migration guidance. + ### Tests Sample tests make important architectural claims repeatable and observable. Typical invariants include: diff --git a/docs/getting-started/toc.yml b/docs/getting-started/toc.yml index 458a619..d515ef1 100644 --- a/docs/getting-started/toc.yml +++ b/docs/getting-started/toc.yml @@ -4,6 +4,8 @@ href: learning-path-map.md - name: Find Your Path href: find-your-path.md +- name: AsiBackbone 6.0 API Boundary + href: asibackbone-6-api-boundary.md - name: Adoption Personas and Entry Points href: adoption-personas-and-entry-points.md - name: Learning Model diff --git a/docs/labs/build-a-governed-api-operation.md b/docs/labs/build-a-governed-api-operation.md index 59e343a..0683b62 100644 --- a/docs/labs/build-a-governed-api-operation.md +++ b/docs/labs/build-a-governed-api-operation.md @@ -665,6 +665,7 @@ A generic boolean does not say what was acknowledged or what operation it belong Create a small challenge model: ```csharp +// Illustrative lab model; not an AsiBackbone package API. public sealed record AcknowledgmentChallenge( string ChallengeId, string ActorId, diff --git a/docs/samples/index.md b/docs/samples/index.md index c029969..e967acb 100644 --- a/docs/samples/index.md +++ b/docs/samples/index.md @@ -7,6 +7,8 @@ _disableBreadcrumb: true Executable samples are the **runnable demonstration layer** of AsiBackbone Learning. +> **Code scope:** These projects compile Learning-owned, framework-neutral teaching models. They do not currently reference `AsiBackbone.*` packages, and similarly named local types are not package API signatures. See the [AsiBackbone 6.0 API Boundary](../getting-started/asibackbone-6-api-boundary.md) for exact current API names, namespaces, and supported construction patterns. + They sit between the problem-first tutorials and the hands-on labs: ```text diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 6c89906..d5935d4 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -8,6 +8,8 @@ AsiBackbone Learning tutorials are **problem-first**. They begin with an archite The goal is understanding—not framework adoption. +> **Code scope:** Tutorial snippets and companion projects are Learning-owned teaching models unless a section is explicitly labeled **AsiBackbone 6.0 API**. For exact current namespaces and syntax, use the [AsiBackbone 6.0 API Boundary](../getting-started/asibackbone-6-api-boundary.md). + ## Learning Path at a Glance | Step | Tutorial | Difficulty | Boundary added | diff --git a/samples/README.md b/samples/README.md index 5f39ea5..ff75fd3 100644 --- a/samples/README.md +++ b/samples/README.md @@ -4,6 +4,8 @@ The `samples/` directory is the executable companion-code area for **AsiBackbone It is intended to contain intentionally small .NET examples that complement the architectural tutorials and make important system boundaries observable through runnable code and tests. +> **Code scope:** Sample projects compile Learning-owned, framework-neutral teaching models. They do not currently reference `AsiBackbone.*` packages, and similarly named local types are not package API signatures. Use the [AsiBackbone 6.0 API Boundary](../docs/getting-started/asibackbone-6-api-boundary.md) for exact current namespaces, evaluator construction, endpoint markers, and migration guidance. + > **Read it. Run it. Question it. Improve it.** ## Current Status diff --git a/tools/validate-asibackbone-6-api-references.cs b/tools/validate-asibackbone-6-api-references.cs new file mode 100644 index 0000000..40b8e20 --- /dev/null +++ b/tools/validate-asibackbone-6-api-references.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +return AsiBackboneApiReferenceValidator.Run(); + +static class AsiBackboneApiReferenceValidator +{ + private const string ApiBoundaryRelativePath = + "docs/getting-started/asibackbone-6-api-boundary.md"; + + private static readonly HashSet ForbiddenCurrentSymbols = new(StringComparer.Ordinal) + { + "AsiBackboneAcknowledgmentChallenge", + "AsiBackboneAcknowledgmentChallengeOptions", + "AsiBackboneAcknowledgmentChallengeRequest", + "AsiBackboneAcknowledgmentChallengeResult", + "AsiBackboneActorContext", + "AsiBackboneActorType", + "AsiBackboneAspNetCoreOptions", + "AsiBackboneAuditLedgerMetadataEntity", + "AsiBackboneAuditLedgerMetadataEntityConfiguration", + "AsiBackboneAuditLedgerReasonCodeEntity", + "AsiBackboneAuditLedgerReasonCodeEntityConfiguration", + "AsiBackboneAuditLedgerRecordEntity", + "AsiBackboneAuditLedgerRecordEntityConfiguration", + "AsiBackboneAuditResidueLifecycleEventEntity", + "AsiBackboneAuditResidueLifecycleEventEntityConfiguration", + "AsiBackboneAuditSinkContract", + "AsiBackboneConstraintContract", + "AsiBackboneConstraintEvaluationContext", + "AsiBackboneContractViolationException", + "AsiBackboneDecisionContract", + "AsiBackboneDecisionPolicyContract", + "AsiBackboneEndpointCapabilityGrantValidatorContract", + "AsiBackboneEndpointGovernanceApplicationBuilderExtensions", + "AsiBackboneEndpointGovernanceDescriptor", + "AsiBackboneEndpointGovernanceMetadataMode", + "AsiBackboneEndpointGovernanceMiddleware", + "AsiBackboneEndpointGovernanceOptions", + "AsiBackboneEndpointGovernanceResult", + "AsiBackboneEndpointGovernanceRouteBuilderExtensions", + "AsiBackboneEntity", + "AsiBackboneGovernanceOutboxDrain", + "AsiBackboneGovernanceOutboxDrainHostedService", + "AsiBackboneGovernanceOutboxDrainWorkerOptions", + "AsiBackboneGovernanceOutboxEntryEntity", + "AsiBackboneGovernanceOutboxEntryEntityConfiguration", + "AsiBackboneGovernanceOutboxOptions", + "AsiBackboneHandshakeAcknowledgmentEntity", + "AsiBackboneHandshakeAcknowledgmentEntityConfiguration", + "AsiBackboneHandshakeAcknowledgmentMetadataEntity", + "AsiBackboneHandshakeAcknowledgmentMetadataEntityConfiguration", + "AsiBackboneHandshakeRequestEntity", + "AsiBackboneHandshakeRequestEntityConfiguration", + "AsiBackboneHandshakeRequestMetadataEntity", + "AsiBackboneHandshakeRequestMetadataEntityConfiguration", + "AsiBackboneHttpActorContextOptions", + "AsiBackboneHttpRequestCorrelation", + "AsiBackboneHttpRequestCorrelationAuditExtensions", + "AsiBackboneHttpRequestMetadataKeys", + "AsiBackboneHttpResultMappingExtensions", + "AsiBackboneHttpResultMappingOptions", + "AsiBackboneIdentifierLimits", + "AsiBackbonePolicyEvaluatorBuilder", + "AsiBackbonePolicyEvaluatorContract", + "AsiBackbonePolicyEvaluatorOptions", + "AsiBackboneSchemaVersions", + "AsiBackboneTestAuditSink", + "AsiBackboneTestHarnessEndpointCapabilityGrantValidator", + "AsiBackboneTestHarnessOptions", + "AsiBackboneTestHarnessPolicyEvaluator", + "AsiBackboneTestHarnessServiceCollectionExtensions", + "AsiBackboneTestSigningService", + "AuditResidue", + "AuditResidueBuilder", + "AuditResidueLifecycleEvent", + "AuditResidueLifecycleStage", + "BackboneResult", + "DefaultAsiBackboneAcknowledgmentChallengeService", + "DefaultAsiBackboneDlpFailurePolicyResolver", + "DefaultAsiBackboneEndpointGovernanceService", + "DefaultAsiBackbonePolicyEvaluator", + "EfCoreAuditResidueLifecycleStore", + "HttpContextAsiBackboneActorContextResolver", + "HttpContextAsiBackboneRequestCorrelationResolver", + "IAsiBackboneAcknowledgmentChallengeService", + "IAsiBackboneActorContext", + "IAsiBackboneAuditLedgerStore", + "IAsiBackboneAuditResidue", + "IAsiBackboneAuditResidueLifecycleStore", + "IAsiBackboneAuditSink", + "IAsiBackboneConstraint", + "IAsiBackboneConstraintEvaluationContext", + "IAsiBackboneDecisionPolicy", + "IAsiBackboneDlpFailurePolicyResolver", + "IAsiBackboneEndpointAuditEmissionMetadata", + "IAsiBackboneEndpointCapabilityGrantMetadata", + "IAsiBackboneEndpointCapabilityGrantValidator", + "IAsiBackboneEndpointGovernanceMetadata", + "IAsiBackboneEndpointGovernancePolicyMetadata", + "IAsiBackboneEndpointGovernanceService", + "IAsiBackboneEndpointLiabilityHandshakeMetadata", + "IAsiBackboneEndpointPolicyEvaluationOptionsMetadata", + "IAsiBackboneEntity", + "IAsiBackboneGovernanceEmitter", + "IAsiBackboneGovernanceOutboxClaimOutcomeStore", + "IAsiBackboneGovernanceOutboxClaimStore", + "IAsiBackboneGovernanceOutboxStore", + "IAsiBackboneHttpActorContextResolver", + "IAsiBackboneHttpRequestCorrelationResolver", + "IAsiBackbonePolicyEvaluator", + "IAsiBackboneSignatureVerificationService", + "IAsiBackboneSigningService", + "InMemoryAuditResidueLifecycleStore", + "RequireGovernancePolicy" + }; + + private static readonly HashSet TextExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".cs", + ".csproj", + ".html", + ".js", + ".json", + ".md", + ".props", + ".svg", + ".targets", + ".tmpl", + ".xml", + ".yaml", + ".yml" + }; + + private static readonly Regex IdentifierRegex = new( + @"\b[A-Za-z_][A-Za-z0-9_]*\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex StaleImplementationLinkRegex = new( + @"https://github\.com/AsiBackbone/AsiBackbone/(?:blob|tree)/(?!release/6\.0\.0(?:/|\b))[^\s)\]'>]+", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + public static int Run() + { + string repositoryRoot; + + try + { + repositoryRoot = FindRepositoryRoot(Environment.CurrentDirectory); + } + catch (InvalidOperationException exception) + { + Console.Error.WriteLine(exception.Message); + return 1; + } + + var errors = new List(); + IReadOnlyList textFiles = EnumerateTextFiles(repositoryRoot).ToArray(); + + ValidateCurrentSymbolsAndLinks(repositoryRoot, textFiles, errors); + int packageReferenceCount = ValidatePackageReferences(repositoryRoot, errors); + ValidateScopeNotices(repositoryRoot, errors); + + if (errors.Count > 0) + { + Console.Error.WriteLine("AsiBackbone 6.0 API-reference validation failed:"); + + foreach (string error in errors) + { + Console.Error.WriteLine($"- {error}"); + } + + Console.Error.WriteLine(); + Console.Error.WriteLine( + $"Use '{ApiBoundaryRelativePath}' for the current names, supported construction paths, and historical-removal inventory."); + return 1; + } + + string packageSummary = packageReferenceCount == 0 + ? "no AsiBackbone package references (framework-neutral sample policy)" + : $"{packageReferenceCount} AsiBackbone 6.x package reference(s)"; + + Console.WriteLine( + $"Validated AsiBackbone 6.0 API references across {textFiles.Count} instructional file(s): {packageSummary}."); + return 0; + } + + private static void ValidateCurrentSymbolsAndLinks( + string repositoryRoot, + IEnumerable files, + ICollection errors) + { + foreach (string path in files) + { + string relativePath = NormalizeRelativePath(repositoryRoot, path); + + if (string.Equals(relativePath, ApiBoundaryRelativePath, StringComparison.Ordinal) || + string.Equals(relativePath, "tools/validate-asibackbone-6-api-references.cs", StringComparison.Ordinal)) + { + continue; + } + + string text = File.ReadAllText(path); + string[] lines = File.ReadAllLines(path); + + for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++) + { + string line = lines[lineIndex]; + + foreach (Match identifierMatch in IdentifierRegex.Matches(line)) + { + if (ForbiddenCurrentSymbols.Contains(identifierMatch.Value)) + { + errors.Add( + $"{relativePath}:{lineIndex + 1} uses removed or renamed 5.x symbol '{identifierMatch.Value}'."); + } + } + } + + foreach (Match linkMatch in StaleImplementationLinkRegex.Matches(text)) + { + int lineNumber = GetLineNumber(text, linkMatch.Index); + errors.Add( + $"{relativePath}:{lineNumber} links implementation source outside release/6.0.0: {linkMatch.Value}"); + } + } + } + + private static int ValidatePackageReferences( + string repositoryRoot, + ICollection errors) + { + var centralVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); + var references = new List(); + + foreach (string path in Directory.EnumerateFiles(repositoryRoot, "*.props", SearchOption.AllDirectories) + .Concat(Directory.EnumerateFiles(repositoryRoot, "*.csproj", SearchOption.AllDirectories)) + .Where(path => !IsExcludedPath(repositoryRoot, path))) + { + XDocument document; + + try + { + document = XDocument.Load(path, LoadOptions.SetLineInfo); + } + catch (Exception exception) when (exception is IOException or System.Xml.XmlException) + { + errors.Add($"Could not parse {NormalizeRelativePath(repositoryRoot, path)}: {exception.Message}"); + continue; + } + + foreach (XElement element in document.Descendants()) + { + string localName = element.Name.LocalName; + + if (localName is not ("PackageReference" or "PackageVersion")) + { + continue; + } + + string? packageId = element.Attribute("Include")?.Value; + + if (!IsAsiBackbonePackage(packageId)) + { + continue; + } + + string? version = element.Attribute("Version")?.Value; + + if (localName == "PackageVersion") + { + if (!string.IsNullOrWhiteSpace(version)) + { + centralVersions[packageId!] = version; + } + + continue; + } + + references.Add(new PackageReference( + packageId!, + version, + NormalizeRelativePath(repositoryRoot, path))); + } + } + + foreach (PackageReference reference in references) + { + string? version = reference.Version; + + if (string.IsNullOrWhiteSpace(version)) + { + centralVersions.TryGetValue(reference.PackageId, out version); + } + + if (string.IsNullOrWhiteSpace(version)) + { + errors.Add( + $"{reference.RelativePath} references {reference.PackageId} without a pinned package version."); + continue; + } + + string normalizedVersion = version.Trim().TrimStart('[', '('); + + if (!normalizedVersion.StartsWith("6.", StringComparison.Ordinal)) + { + errors.Add( + $"{reference.RelativePath} references {reference.PackageId} {version}; Learning package-integration samples must use 6.x."); + } + } + + return references.Count; + } + + private static void ValidateScopeNotices( + string repositoryRoot, + ICollection errors) + { + string[] noticePaths = + { + "docs/tutorials/index.md", + "docs/samples/index.md", + "samples/README.md" + }; + + foreach (string relativePath in noticePaths) + { + string path = Path.Combine( + repositoryRoot, + relativePath.Replace('/', Path.DirectorySeparatorChar)); + + if (!File.Exists(path)) + { + errors.Add($"Missing code-scope notice location '{relativePath}'."); + continue; + } + + string text = File.ReadAllText(path); + + if (!text.Contains("Learning-owned", StringComparison.Ordinal) || + !text.Contains("asibackbone-6-api-boundary.md", StringComparison.OrdinalIgnoreCase)) + { + errors.Add( + $"{relativePath} must distinguish Learning-owned code from the AsiBackbone 6.0 API and link the API boundary guide."); + } + } + } + + private static IEnumerable EnumerateTextFiles(string repositoryRoot) + { + return Directory + .EnumerateFiles(repositoryRoot, "*", SearchOption.AllDirectories) + .Where(path => !IsExcludedPath(repositoryRoot, path)) + .Where(path => TextExtensions.Contains(Path.GetExtension(path))); + } + + private static bool IsExcludedPath(string repositoryRoot, string path) + { + string relativePath = NormalizeRelativePath(repositoryRoot, path); + string[] segments = relativePath.Split('/'); + + return segments.Any(segment => + segment.Equals(".git", StringComparison.OrdinalIgnoreCase) || + segment.Equals("_site", StringComparison.OrdinalIgnoreCase) || + segment.Equals("bin", StringComparison.OrdinalIgnoreCase) || + segment.Equals("obj", StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsAsiBackbonePackage(string? packageId) + { + return !string.IsNullOrWhiteSpace(packageId) && + (packageId.Equals("AsiBackbone", StringComparison.OrdinalIgnoreCase) || + packageId.StartsWith("AsiBackbone.", StringComparison.OrdinalIgnoreCase)); + } + + private static int GetLineNumber(string text, int characterIndex) + { + int lineNumber = 1; + + for (int index = 0; index < characterIndex; index++) + { + if (text[index] == '\n') + { + lineNumber++; + } + } + + return lineNumber; + } + + private static string NormalizeRelativePath(string repositoryRoot, string path) + { + return Path.GetRelativePath(repositoryRoot, path).Replace('\\', '/'); + } + + private static string FindRepositoryRoot(string startDirectory) + { + DirectoryInfo? directory = new(startDirectory); + + while (directory is not null) + { + if (Directory.Exists(Path.Combine(directory.FullName, ".git")) || + File.Exists(Path.Combine(directory.FullName, ".git"))) + { + return directory.FullName; + } + + directory = directory.Parent; + } + + throw new InvalidOperationException( + $"Could not locate the repository root from '{startDirectory}'."); + } + + private sealed record PackageReference( + string PackageId, + string? Version, + string RelativePath); +}