Repository navigation
chore(release): bump VERSION to 0.2.0 and correct the release record - #628
Merged
Merged
Conversation
VERSION read 0.1.2 while 0.1.2 was already published on nuget.org, so the version string in this tree no longer identified its content. THE MINOR MOVED RATHER THAN THE PATCH, because signed bytes changed. #592 gave every double one canonical decimal form across all three target frameworks: the netstandard2.0 asset under Mono had been writing different digits from net8.0/net10.0 for the same value, and the canonical payload is the message every certification signature is computed over. Both golden corpora changed with it. #621 compounds it - a host that supplied a brick signer previously got composition records under the committed dev key and now gets them under its own. A record minted under 0.1.2 and the same record minted now are not byte-identical, so 0.1.3 would have told a consumer holding stored signed records that this was a safe patch. Corrections landed in the same pass, each verified against the artefact rather than against a status document: - Section 5.4b's counts - 104 commits past v0.1.2, seven touching src/Ashlar.Certification.Contracts/ - were ACCURATE when recorded on 2026-09-13. A draft of this commit "corrected" them to 37 and 3; that was wrong, measured in a shallow clone whose master stops at 37 commits. In a full clone it is 112 and 7. The body now carries no count, since it changes on every merge, and the note says to re-derive it in a full clone - a shallow checkout also makes `git describe` find no tag at all, which is how the bad measurement looked plausible. - Section 5.4b said scripts/release-preflight-local.sh "is the only thing that inspects both". It is not: release-staging-on-label.yml resolves the canonical version from root VERSION and runs automatically when a PR is labelled, and resolve-canonical-package-version.sh, verify-docs-published-version.sh and tests/uat/tier9.sh read it too. What none of them do is compare VERSION to the latest tag, which is the actual gap the section exists to name. - README's self-extend bullet claimed the operator-governed path has "a post-apply canary that auto-rolls-back a change that fails verification", unconditionally. SelfExtendAdmissionBridge takes IPostApplyVerification as an optional parameter defaulting to null, and rolls back only "when verification is not null". AddCertificationInfrastructure supplies one, so the supported wiring has the canary; a host constructing the bridge by hand and passing nothing gets no canary and no rollback, silently. Scoped to what holds. - Four go-criteria were met and left unticked: security defaults documented, NuGet 0.1.2 published, GHCR images published, GHCR images available. Ticked with one line of evidence each. The GHCR line now also records that nexo-api is amd64-only by design, which a reader would otherwise infer from the multi-arch claim beside it. A no-go item whose own text read "resolved" was still an open checkbox; it is closed. Verified in-container: cert-gate 922/922. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IanFrelinger
force-pushed
the
chore/release-record-truth
branch
from
September 14, 2026 23:06
66d44b3 to
5ba174f
Compare
…st the guards Two gaps docs/release/RELEASE-READINESS.md named and nothing closed. 1. NOTHING WARNED THAT VERSION WAS STALE. release.yml asserts a tag matches root VERSION, but only at tag time and only when ref_type == 'tag' - by then the tag exists and the guard's only move is to refuse the release. That is the good failure, but it fails late, and it is how 0.1.2 came to sit in the tree while 0.1.2 was already published. scripts/check-version-staleness.sh compares VERSION to the newest v* tag and to nuget.org. It runs from release-preflight-local.sh - the last human checkpoint before a tag - and from rc-gate. Advisory by design: it always exits 0. Whether a tree is "too far" past its tag is a judgement call, and a check that reddens a lane on a judgement call is how lanes get muted in this repository (docs/HowGatesGoQuiet.md). It needs a FULL clone, and says so when it does not have one. A shallow checkout truncates the history, git describe then finds no tag, and the check silently measures nothing - which is not hypothetical: a draft of the VERSION bump in #628 "corrected" this section's commit counts using numbers measured in a shallow clone, where master stops at 37 commits. The rc-gate checkout sets fetch-depth: 0 and fetch-tags for exactly this reason. 2. THE RELEASE GUARDS HAD NO TESTS. scripts/lib/release-staging-guards.sh stands between a mislabelled package and nuget.org, and was only ever exercised as a side effect of a real release - which has run three times in this repository's lifetime. A guard exercised only by the event it guards is indistinguishable from a guard that does nothing. tests/scripts/release-guards.test.sh covers normalize_semver, assert_valid_semver and assert_version_matches_canonical: matching tag exits 0, mismatched exits nonzero AND says "does not match", an ABSENT VERSION file exits 0 with SKIPPED while an EMPTY one refuses, and a bare version still matches a v-prefixed tag. It also pins that the three nuget-visibility scripts raise a short poll budget unless ASHLAR_NUGET_VERIFY_ALLOW_SHORT=1 - nuget.org indexing lags publication, and a short budget reports "not visible" for a package that published fine. 21 assertions, pure bash, no network or dotnet, so they run in shell-lint on every pull request rather than in a monthly lane. Mutation-measured: disabling the mismatch check fails exactly one assertion and leaves 20 passing. shell-lint's bash -n sweep covered scripts/ only, so tests/ shell was not even syntax-checked. Widened to both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rkflows by line Section 5.4 is headed "Repository variables / secrets (release workflow)" and listed six. The release workflows read FIFTEEN. The nine missing ones are added, each with what UNSET means, because for most of them unset is the intended state and the default is the behaviour you get: - NUGET_POST_PUSH_VERIFY and RELEASE_CROSS_VERIFY both test != 'false', so only the literal string false disables them. A reader who set either to 0 or off would believe it was disabled and be wrong. - NUGET_POST_PUSH_ATTEMPTS (40) and NUGET_POST_PUSH_SLEEP_SEC (15) are the ~10-minute nuget.org visibility budget. nuget.org indexing lags publication, so a shortened budget reports "not visible" for a package that published fine - which reads as a failed release. - NUGET_STAGING_FEED_URL and NUGET_STAGING_API_KEY are the only way to rehearse a publish against something that is not nuget.org, and neither was documented. - RELEASE_NOTIFICATION_WEBHOOK_URL, NUGET_POST_PUSH_VERIFY_PACKAGE_IDS, and GITHUB_TOKEN for completeness, the last so its absence from the configurable rows is not read as an omission. Every value re-read from the live API on 2026-09-14: three variables and one secret exist; the other eleven are genuinely unset. The section also cited three workflow lines - reusable-release-nuget.yml:226, :185 and release.yml:198. All three resolved correctly today, and all three will rot the next time those files are edited, which is the defect this document has already been corrected for twice. They now name the step or the condition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the A4 claim docs/SELF-EXTEND-AUDIT.md is named by the public-v0.x go criterion "Known limitations documented: certification-evidence.md + SELF-EXTEND-AUDIT.md current", so it is read as live rather than as a dated sprint record. Its control-flow trace carries 28 file.cs:line anchors from the audit date. Measured 2026-09-14: five now land on a bare brace or a blank line, and landing on real code is not the same as landing on the code described - BackgroundAgentRegistry.cs:203 is an ArgumentNullException throw where the trace describes machine-origin agent validation. Annotated rather than re-pinned. That is this repository's policy for dated documents, and re-pinning has already failed twice in this tree: a re-anchoring in docs/certification-evidence.md went stale within hours of being written, and a correction to two line ranges in ci/cert-gate-assertions.md was itself wrong. Member and type names are stable; the note says to search for those. The note also scopes A4. The audit lists "Post-apply verification with automatic rollback exists and is tested" among the enforced invariants, which reads as unconditional. SelfExtendAdmissionBridge takes IPostApplyVerification as an optional parameter defaulting to null and rolls back only "when verification is not null". AddCertificationInfrastructure supplies one, so the supported wiring has the canary; a host constructing the bridge by hand and passing nothing gets no canary and no rollback, silently. README carried the same overclaim and is corrected in the same branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IanFrelinger
pushed a commit
that referenced
this pull request
Oct 1, 2026
…ing and deprecation suppressions
WHY. Ashlar.Brick.Contracts is a stable-tier package, and the mechanism
docs/SdkCompatibilityPolicy.md names for its promise is the public-API analyzer:
RS0016 fails the build when a public symbol is in neither PublicAPI file. The five
files under src/Ashlar.Brick.Contracts/Authoring/Ports/ opened with a file-wide
`#pragma warning disable RS0016` that was never restored ("Profile ports ship via
Unshipped incrementally; suppress until batched" - the batch never came). None of
their lines was in either PublicAPI file, so 18 public types - AgentProfile,
AgentProfileCapabilities, GenerationTunables, BrickConstraintManifest,
GenerationRequest, GeneratedArtifact, AcceptanceDecision, DeploymentSmokeResult,
AcceptanceContext, AcceptanceResult, IAcceptanceEvaluator,
DefaultAcceptanceEvaluator, IAcceptanceGatedDeploymentTarget, IArtifactDrafter,
IDeterministicDrafter, ISandboxProvider, IDeploymentTarget, DeploymentApplyResult
(namespace Ashlar.Core.Domain.Bricks.Ports) - were promised by nothing, and any
change to them built green. With 0.2.0 about to publish, the promise in the policy
was false for them.
SHIPPED, NOT UNSHIPPED - the evidence.
- `git ls-tree v0.1.0|v0.1.1|v0.1.2 -- src/Ashlar.Brick.Contracts/Authoring/Ports/`
lists all five files at every tag, and both `git diff v0.1.0 v0.1.2 -- .../Ports/`
and `git diff v0.1.2 f0fd3fd -- src/Ashlar.Brick.Contracts/` are empty. Every one
of these symbols was public, unchanged, in three tagged releases the policy
records as published. That is a promise already made: Shipped.txt.
- Nothing in this surface is new since 0.1.2, so nothing goes in Unshipped.txt, and
that is also what keeps the 0.2.0 tag commit right. The 0.2.0 cut is already on
master (#628 set VERSION=0.2.0; #678 "release: cut 0.2.0" set
ci/published-version=0.2.0 and recorded that PublicAPI promotion was a no-op),
and RELEASE_RUNBOOK step 7's promotion has already left every Unshipped.txt at
the bare header - no PublicAPI file has changed since. A line put in
Unshipped.txt now would reach the 0.2.0 tag unpromoted unless someone re-ran
step 7, and the tag is what turns a line into a promise; so a symbol genuinely
new since 0.1.2 would belong in Shipped.txt as well. There is none here.
- The 147 lines are the analyzer's own, not hand-written: read from the RS0016
diagnostics' APIName property (SARIF via -p:ErrorLog), 144 names identical for
netstandard2.0, net8.0 and net10.0, plus the three positional-record primary
constructors the analyzer only reports once their types are declared. `dotnet
format analyzers --diagnostics RS0016` applied no fix to this multi-targeted
project (measured; also single-TFM), so it was not used. The lines carry nullable
annotations (`string!`) as the analyzer emits them and as the Abstractions,
Client, Sdk and Authoring files already do; the older oblivious Brick.Contracts
lines are left alone. Shipped.txt: 465 -> 612 lines.
- Not [Experimental]: demoting a shipped stable API is a breaking change under the
policy, and it would not be true - these are in use as a stable contract.
THE DEAD CS0618 PRAGMA. src/Ashlar.Hosting/Sdk/Builders/HostAshlarSdkBuilder.cs:7
disabled CS0618 because "AshlarSdkBuilder is an obsolete type forwarder in this
file". #223 (2026-07-04) moved the forwarder into AshlarSdkBuilder.cs and the pragma
stayed, suppressing nothing for 88 days while reading as a reason - and silencing
in advance the next obsolete call anyone wrote into the file. Deleted. Measured:
Ashlar.Hosting builds with 0 warnings / 0 errors without it on net8.0 and net10.0.
THE PREVENTION: DiagnosticSuppressionConventionTests (cert-gate namespace, row in
ci/cert-gate-assertions.md).
- Rule 1: nothing switches public-API tracking off. No source anywhere disables
RS0016/RS0017/RS0026/RS0027 by pragma or [SuppressMessage]/
[UnconditionalSuppressMessage], and no source has a bare `#pragma warning
disable`; no MSBuild file lists those ids in NoWarn/WarningsNotAsErrors; no
.editorconfig/.globalconfig lowers them to none/silent/suggestion or sets
`dotnet_public_api_analyzer.skip_namespaces`, and none that applies to an
analyzer project lowers the ApiDesign category or every analyzer; no analyzer
project or Directory.Build.* above one sets RunAnalyzers(DuringBuild) false or
removes an API file from AdditionalFiles; and each of the 6 projects referencing
the analyzer wires both API files as AdditionalFiles. The source scan is
tree-wide on purpose: a pragma acts on its own file, files are linked into these
projects from outside them, and an RS0016 pragma anywhere else is dead by
definition.
- Rule 2: a `#pragma warning disable` of CS0618 or ASHLAREXP001 - in every spelling
the compiler accepts, which for CS0618 includes the bare number: Roslyn maps a
numeric pragma id (and a numeric /nowarn id) to "CS" + the number padded to four
digits, so `618` and `0618` ARE CS0618 - must name, as an exact identifier, an
in-tree [Obsolete] (CS0618 - not error: true, no DiagnosticId) or
[Experimental(ASHLAREXP001)] symbol; at least one named symbol must be used in
the code the pragma governs OUTSIDE a declaration carrying the same attribute
(the compiler reports neither diagnostic in an obsolete/experimental context);
and the comment must give a reason of at least three words. Tree-wide, because
the defect it exists for was in Ashlar.Hosting, which does not use the API
analyzer.
- Rule 2 also covers the disables that cannot carry such a comment: a
[SuppressMessage]/[UnconditionalSuppressMessage] naming either id; a
NoWarn/WarningsNotAsErrors element or attribute listing either id (any
spelling) in any MSBuild file; and a `dotnet_diagnostic.<id>.severity` of
none/silent/suggestion in any analyzer config. A project-wide disable names no
symbol by construction, so every one must be on ReviewedProjectWideDisables,
which records why. Two exist and both are deliberate, documented opt-ins:
Directory.Build.targets:32 (test projects exercise the experimental surface;
the diagnostic exists for external consumers) and the FirstFlight spike's
csproj:14 (the spike is a consumer, outside Ashlar.sln and every gate).
- Eight existing pragmas fail Rule 2 in files outside this change; they are
grandfathered by exact text, each with what clears it. Both lists fail in both
directions (a new violation, or an entry that no longer matches), so each can
only change by a reviewed diff. Compiler ground truth, measured by deleting each
pragma and building on f0fd3fd: both Ashlar.Sdk/Legacy CS0618 pragmas are DEAD
(Ashlar.Sdk builds clean - every use sits inside an [Obsolete] context); the
test one is live but uncommented; all six ASHLAREXP001 pragmas are live, and
five of them name no symbol ("touch-set", "lineage"). ObjectiveDocument.cs
already complies and is asserted to pass.
- Stated floors, not derived ones (2500 C# files, 100 MSBuild files, 6 analyzer
projects, 40 experimental names, 1 obsolete name, 10 disable pragmas; measured
3149, 133, 6, 65, 2, 15). The obsolete floor is 1, under the measured 2, so that
retiring one forwarder - the normal end of a deprecation - is not reported as a
broken parser; the real-tree anchors (AshlarSdkBuilder, TouchSet, ...) say in
their failure messages that a removed or graduated symbol means "move the
anchor", not "the parser is broken". Controls over in-memory text pin every
banned spelling (including 618/0618 and a numeric restore), immunity to
comments and literals (including raw and verbatim strings), the obsolete-context
rule, CS0619/DiagnosticId exclusion, the project-wide and attribute forms and
what they must not catch (CS8618, 6180, WarningsAsErrors, a commented-out
NoWarn), and the shipped HostAshlarSdkBuilder.cs verbatim as a dead pragma in
all its spellings.
VERIFIED in the devtest container on the committed tree:
- Builds, 0 warnings / 0 errors: Brick.Contracts on netstandard2.0, net8.0 and
net10.0; Hosting on net8.0 and net10.0; Tests.Infrastructure on net8.0 and
net10.0. Ashlar.LocalDevCore.slnf (what build-core builds) restores and builds,
0 errors. The class passes 96/96 on net8.0 and on net10.0 (TRX executed=96).
scripts/run-cert-gate.sh exits 0: total=1410 executed=1410
passed=1410 failed=0 (the class's 96 among them), zero-test guard 1410 against
expected>=1405 from --list-tests, skip guard 0 skipped, matching
scripts/cert-gate-skipped.baseline.
- Mutations, each proven applied by the original string's count moving, each
restored to an empty `git status --porcelain`, then rerun green:
* the shipped dead pragma back at HostAshlarSdkBuilder.cs:7 as
`#pragma warning disable 618 // AshlarSdkBuilder is ...` -> the Rule 2 fact
fails (95/96): "HostAshlarSdkBuilder.cs:7: '#pragma warning disable 618 ...'
- CS0618: is dead". Review measured the first round passing 63/63 on it.
* a new `new AshlarSdkBuilder(null!)` in Ashlar.Hosting under a bare
`#pragma warning disable 618` -> Hosting builds exit 0 (the compiler IS
silenced) and the Rule 2 fact fails: "ApivProbe.cs:1: '#pragma warning
disable 618' - CS0618: carries no comment". Without the pragma, Hosting
fails with CS0618 on net8.0 and net10.0 (2 errors).
* `<NoWarn>$(NoWarn);CS0618</NoWarn>` in Ashlar.Hosting.csproj with that use
present -> Hosting builds exit 0 and the project-wide fact fails naming
"Ashlar.Hosting.csproj:9"; spelled `618`, it fails the same way.
* `dotnet_diagnostic.CS0618.severity = none` in the root .editorconfig -> the
project-wide fact fails naming ".editorconfig:188".
* FirstFlight's reviewed NoWarn deleted -> the project-wide fact fails: "is on
the reviewed list but no longer found".
* an assembly-level [SuppressMessage("Usage", "CS0618:...")] -> the Rule 2 fact
fails naming it.
* numeric-id normalisation disabled -> exactly the 9 numeric-spelling controls
fail (87/96); the project-wide element scan disabled -> the real-tree
project-wide fact fails with BOTH reviewed entries stale, plus the 5 element
controls (90/96).
* from the first round, still standing: RS0016 pragma back on AgentProfile.cs:1
-> Rule 1 fails naming it; a new public property on GenerationTunables ->
Brick.Contracts fails RS0016 for Extra.get/Extra.init on all three TFMs, and
the SAME change with the old pragma restored builds 0/0 - what the
suppression hid; one Shipped.txt line removed -> RS0016 on all three TFMs;
RS0016 in the project's NoWarn -> Rule 1 fails; a Ports-scoped
skip_namespaces -> the property's RS0016 disappears (the option really
untracks) and Rule 1 fails naming the line; the eight grandfathered pragmas
rewritten as their notes say -> eight "no longer violates", and with the
list emptied it passes; declaration parser broken -> the floor fires;
obsolete-context rule removed -> exactly its control fails; literals not
blanked -> six facts fail.
- scripts/ci/run-repo-gates.sh: all 21 gates pass. Knowledge graph regenerated
(declared facts 4399 -> 4414) and verified current.
WHAT THIS DOES NOT FIX.
- The eight grandfathered pragmas (two to delete, six to name their symbols) and
docs/SdkCompatibilityPolicy.md, which still quotes 465 Shipped lines for
Brick.Contracts and does not list the Ports types - all outside this change.
- Rule 2 is lexical: it proves a disable names a real symbol that the governed code
still mentions outside that symbol's own context, not that the compiler would
report there (a lambda inside an obsolete member, nameof, a same-named symbol
from another assembly all read as uses). An obsolete API declared outside the
tree cannot be named and needs a reviewed grandfather entry. CS0612 (an
[Obsolete] with no message) is a different id and is not policed.
- Neither rule evaluates MSBuild: conditions are ignored (a disable is flagged
whatever its Condition), and a disable routed through a property of another
name (`<NoWarn>$(MyList)</NoWarn>`) or passed on a command line (`-p:NoWarn=`,
`-nowarn:` in a script or workflow) is not seen. TreatWarningsAsErrors,
WarningLevel and the bulk `dotnet_analyzer_diagnostic.*` settings are not
checked for Rule 2. Rule 1 follows no imports beyond the Directory.Build.*
chain and does not treat an <auto-generated/> header as a suppression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
VERSION read 0.1.2 while 0.1.2 was already published on nuget.org, so the
version string in this tree no longer identified its content.
THE MINOR MOVED RATHER THAN THE PATCH, because signed bytes changed. #592 gave
every double one canonical decimal form across all three target frameworks: the
netstandard2.0 asset under Mono had been writing different digits from
net8.0/net10.0 for the same value, and the canonical payload is the message
every certification signature is computed over. Both golden corpora changed with
it. #621 compounds it - a host that supplied a brick signer previously got
composition records under the committed dev key and now gets them under its own.
A record minted under 0.1.2 and the same record minted now are not
byte-identical, so 0.1.3 would have told a consumer holding stored signed
records that this was a safe patch.
Corrections landed in the same pass, each verified against the artefact rather
than against a status document:
Section 5.4b said master was 104 commits past v0.1.2 with seven touching
src/Ashlar.Certification.Contracts/. Measured 2026-09-14: 37 and 3. No count
is recorded now - it changes on every merge, and both previous ones were
wrong. The section says how to re-derive it instead.
Section 5.4b said scripts/release-preflight-local.sh "is the only thing that
inspects both". It is not: release-staging-on-label.yml resolves the canonical
version from root VERSION and runs automatically when a PR is labelled, and
resolve-canonical-package-version.sh, verify-docs-published-version.sh and
tests/uat/tier9.sh read it too. What none of them do is compare VERSION to the
latest tag, which is the actual gap the section exists to name.
README's self-extend bullet claimed the operator-governed path has "a
post-apply canary that auto-rolls-back a change that fails verification",
unconditionally. SelfExtendAdmissionBridge takes IPostApplyVerification as an
optional parameter defaulting to null, and rolls back only "when verification
is not null". AddCertificationInfrastructure supplies one, so the supported
wiring has the canary; a host constructing the bridge by hand and passing
nothing gets no canary and no rollback, silently. Scoped to what holds.
Four go-criteria were met and left unticked: security defaults documented,
NuGet 0.1.2 published, GHCR images published, GHCR images available. Ticked
with one line of evidence each. The GHCR line now also records that nexo-api
is amd64-only by design, which a reader would otherwise infer from the
multi-arch claim beside it. A no-go item whose own text read "resolved" was
still an open checkbox; it is closed.
Verified in-container: cert-gate 922/922.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Verification
cert-gatein-container🤖 Generated with Claude Code