feat(blueprint): specify how an authored parameter binds to component inputs - #36
Merged
Merged
Conversation
… run
Nothing in this repository installed actionlint. Not a devcontainer Feature,
not mise.toml, not any script under .devcontainer/. The binary that answered on
PATH had been hand-placed into the container, was built for x86-64, and died
with `exec format error` on an aarch64 host — so `task check:workflow` could not
run, `task check` could not complete, and the pre-push hook had to be bypassed.
Three changes, and the third is the one that matters.
**mise.toml pins actionlint at 1.7.11**, matching the version ci.yml installs.
mise resolves it per-architecture through the aqua backend and verifies the
release attestation, which is exactly what a hand-fetched binary does not do.
The file's header comment scoped it to "npm-distributed" CLIs; devcontainer.json
designates it for any tool with no Feature, so the comment is widened to say so
and to record that ci.yml carries the same version independently.
**Taskfile.yml drops LEFTHOOK_VERSION and ACTIONLINT_VERSION.** Neither was read
by any task, script or workflow. ACTIONLINT_VERSION was a declared intent with
nothing enforcing it — the version now lives in mise.toml, where something acts
on it.
**verify_tools now checks that a tool runs, not just that it resolves.** The old
version logged `$("$cmd" --version || echo installed)`, which swallowed the
failure and reported a broken binary as present — it would have called the
x86-64 actionlint installed. Being on the PATH is not the property worth
checking; being runnable is. Every tool it verifies exits 0 on `--version`, so a
non-zero status means the binary cannot execute, and that now fails the build
where it belongs rather than at someone's first push. actionlint and shellcheck
join the verified list, since `task check` runs both.
Refs #20
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
Bumps [go-task/setup-task](https://github.com/go-task/setup-task) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/go-task/setup-task/releases) - [Changelog](https://github.com/go-task/setup-task/blob/main/CHANGELOG.md) - [Commits](go-task/setup-task@01a4adf...a00fbb0) --- updated-dependencies: - dependency-name: go-task/setup-task dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> (cherry picked from commit cce11df)
Bumps the tooling group in /tools with 1 update: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome). Updates `@biomejs/biome` from 2.5.7 to 2.5.8 - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.8/packages/@biomejs/biome) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.5.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: tooling ... Signed-off-by: dependabot[bot] <support@github.com> (cherry picked from commit 7a580d5)
The bump changed the CLI but not the $schema URL the config pins, so `biome ci` reported a version mismatch on every run. `biome migrate` closes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
… inputs §5 settled that a non-empty `spec.parameters` is "an authored override, used in place of derivation rather than merged with it", and §5.1 and §5.2 then specified derivation in full — which inputs are derived, in what order, and what happens when two nodes disagree. The override path had none of that, and §5.2's own closing line recommends it. The specification recommended a path it did not define. New §5.3 defines it. Binding is by key, and the key is the whole of the correspondence: a parameter carries no `suppliedBy`, no node name and no `target`, so there is nothing else to bind on. That is also what lets one parameter serve two components, which is the whole of why §5.2 sends an author here. Three semantic rules follow, each needing the referenced component documents. **A parameter MUST cover something** — ERR_UNBOUND_PARAMETER, anchored at /spec/parameters/<key>. The install form asks a deploying user for a value and nothing reads it. §3 already makes this argument about an unreferenced component document: permitted, these accumulate, and nothing says which entries are live. **An input the deploying user must supply MUST be covered** — ERR_UNCOVERED_REQUIRED_INPUT, anchored at /spec/parameters. An input is in scope when it is USER-supplied, required, and carries no generator, no platformDefault and no schema default: nothing else can supply it, so if the form does not ask, the workload starts without it. This is the mirror of ERR_UNWIRED_REQUIRED_INPUT on the CONNECTION side. Coverage tests what a parameter *guarantees*, not which keys it names, and that is load-bearing rather than fussy. `isRequired` defaults to **true** on a component input and to **false** on a blueprint parameter, so an override that copies a required key and says nothing else has quietly made it optional. Tested by key alone the rule would be evaded by satisfying it. A parameter covers such an input only if it declares `isRequired: true`, carries a generator, or declares a schema default. **`type` MUST agree** — ERR_INCOMPATIBLE_PARAMETER_TYPE. Exactly the axis §4.2 chose for connections, and nothing else compared. The user is validated against the parameter's schema and the component then receives the result against its own; §5.3 records format, enum, pattern, default, isSensitive and ui as silences in the same words §4.2 uses. `semanticType` is not compared because a parameter has none — an install form is not where a value acquires a backing-service tag. **A generated parameter is secret material.** Component §6.1 obliges a generated input to declare `isSensitive: true`, and a derived parameter takes the input's schema unchanged, so the marking is guaranteed on the derivation path. It was not on the authored one — and isSensitive defaults to false, so losing it took no more than not mentioning it. Now structural, both halves: an absent isSensitive is ERR_MISSING_FIELD and one written false is ERR_INVALID_VALUE, the same dual phrasing §4 uses for spec.components. **An override suppresses the merge, and this fixes a live inconsistency.** §5 says derivation does not run; if it does not run there is nothing to merge and nothing to conflict. `semantic.ts` never read spec.parameters, so ERR_CONFLICTING_INPUT_SCHEMA fired regardless — meaning §5.2's own recommended remedy did not remedy. `checkGraphAgainstItem` now takes one path or the other. semantic-020 is the regression pin: two nodes declaring one key with differing patterns, reconciled by an override, which fails against the previous implementation and passes now. It also shows the limit of the remedy — an override reconciles a disagreement below `type`, not one about `type`. Seven fixtures, 107 -> 114 cases, 0 skipped. No new UNCOVERED entry: every code here is semantic or structural and therefore testable. Closes #20 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
`web-and-database.yaml` is the only published example that authors `spec.parameters`, and the block arrived unexplained — a reader met the feature §5.2 recommends with nothing saying how the key reaches an input, or that overriding forgoes the derived set entirely. Now commented: binding is by key, the key is the whole of the correspondence, and the override is a commitment rather than an addition. It also accounts for why `siteTitle` is the only entry, which is the question §5.3's coverage rule makes a reader ask. Nothing checks it either way. `examples/` carries no item root by design (tools/src/validator.ts), so §5.3's rules cannot fire there — which is exactly why the example has to teach the rule in prose rather than by passing. CLAUDE.md's Known debt section said TODO sections remain in blueprint and listing. Blueprint has none left. Refs #20 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
`task env:check` failed against an unmodified checkout, and the Dev Container workflow's Env Template Sync job failed with it. Not drift — it failed when the two files were byte-identical, because copying `.env.example` to `.env` is exactly what the job does first. `env_check_keys` ran `grep | sed | awk` under `set -o pipefail`. grep exits 1 when it matches nothing, so a template declaring no keys aborted the function, and `set -e` took the whole check down before it could conclude there was no drift. A file with no keys is not an error here — it is this repository's documented state: ".env.example" says outright that "there is nothing required here", because the repo runs no services and needs no credentials. The match is now captured before the pipeline, so an empty key set stays a result instead of becoming a failure. `env_check_drift` also drops blank lines from comm's output, since an empty set echoes as one blank line that would otherwise read as a key named "". Pre-existing on main, and only surfaced now because the workflow is path filtered to `.devcontainer/**` and nothing had touched that directory since the template lost its last key. Verified both directions: byte-identical files exit 0, and adding a key to the template alone still reports it and fails. Refs #20 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Merrell <merrelljustin@gmail.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.
What changes
Blueprint §5 defined derivation in full and left the override path — the one §5.2 recommends — undefined. New §5.3 Authored parameters defines it: binding is by key, and three semantic rules follow. Along the way it fixes a live inconsistency that made §5.2's recommended remedy not actually remedy.
Also folds in the dev-container tooling fix that blocked
task checklocally, plus the two open dependabot bumps.Why
Closes #20. Supersedes #33 and #34 (both cherry-picked here with dependabot's authorship intact).
The mechanism was already settled — by the platform, not by us
ParameterBindings.valuesis documented as "Keyed by the inputkeyof eachUSERComponentInput on the template", and one flat mapping is handed to every node, each taking the keys it recognises (blueprint/acl/component/adapter.py). Binding is by key, in one namespace shared across the graph. The seed importer confirms the override is wholesale —parameters = manifest.blueprint.parameters or _derived_parameters(manifest)— and performs no correspondence check at all.So both of the issue's questions are genuine gaps in both projects rather than undocumented behaviour. This is the specification deciding, not transcribing.
§5.3 — three rules
A parameter MUST cover something —
ERR_UNBOUND_PARAMETERat/spec/parameters/<key>. The install form asks a deploying user for a value and nothing in the composition reads it. §3 already makes exactly this argument about an unreferenced component document: permitted, these accumulate, and nothing says which entries are live.An input the deploying user must supply MUST be covered —
ERR_UNCOVERED_REQUIRED_INPUTat/spec/parameters. In scope when the input isUSER-supplied, required, and carries nogenerator, noplatformDefaultand noschema.default— i.e. when nothing but a person can supply it. The mirror ofERR_UNWIRED_REQUIRED_INPUTon theCONNECTIONside, and the same argument: no later chance to supply it.typeMUST agree —ERR_INCOMPATIBLE_PARAMETER_TYPE. Exactly the axis §4.2 chose for connections, and nothing else compared.format,enum,pattern,default,isSensitiveanduiare recorded as silences in §4.2's own words.semanticTypeis not compared because a parameter has none — an install form is not where a value acquires a backing-service tag.The
isRequiredtrap, and why coverage tests guarantees rather than keysisRequireddefaults totrueon a component input and tofalseon a blueprint parameter. An override that copies a required input's key and says nothing else has quietly made it optional.Tested by key alone, the second rule would be evaded by the act of satisfying it: name the key, leave it optional, and the form still never compels the value. So a parameter covers such an input only if it guarantees a value —
isRequired: true, or agenerator, or aschema.default.semantic-018is that case, and it is the one worth reviewing hardest.A hole on the authored path that the derived path did not have
Component §6.1 obliges a generated input to declare
schema.isSensitive: true, and §5.1 says a derived parameter takes the input's schema unchanged — so on the derivation path the marking is guaranteed. On the authored path it was not, andisSensitivedefaults tofalse, so losing it took no more than not mentioning it. Nowstructural, both halves: an absentisSensitiveisERR_MISSING_FIELDand one writtenfalseisERR_INVALID_VALUE— the same dual phrasing §4 already uses forspec.components.Three findings worth a reviewer's attention
1. §5.2's recommended remedy did not work, and this PR is what makes it true. §5.2 tells an author that two components disagreeing on an input schema is resolved by writing
spec.parametersoutright.tools/src/semantic.tsnever readspec.parameters, socheckInputMergeran regardless andERR_CONFLICTING_INPUT_SCHEMAfired anyway. §5 already said an override is "used in place of derivation rather than merged with it" — if derivation does not run, there is nothing to conflict.checkGraphAgainstItemnow takes one path or the other.semantic-020is the regression pin: two nodes declaring one key with differingpatterns, reconciled by an override. It fails against the previous implementation and passes now — verified, not asserted.2.
semantic-020also shows the limit of the remedy. The two nodes disagree onpattern, not ontype. An override can reconcile a disagreement belowtype; it cannot reconcile one abouttype, because rule three would then reject whichever half the parameter picked. That is the right boundary — aSTRINGand anINTEGERare not one value asked for twice — but it is a narrower remedy than §5.2's sentence implies on its own, so §5.3 states it.3. The only published example of an authored override is unverifiable, and now says so.
examples/web-and-database.yamlauthorssiteTitleagainst component documents that deliberately do not exist —examples/carries no item root by design, so §5.3's rules cannot fire there. The block arrived with no comment at all. It now explains the by-key correspondence and whysiteTitleis the only entry, so it teaches the rule rather than sitting beside it unexplained.Tooling, in the same PR
actionlintwas orphaned. Nothing in the repo installed it — not a Feature, notmise.toml, not any.devcontainerscript. The binary answering onPATHwas hand-placed, built for x86-64, and died withexec format erroron this aarch64 host, sotask checkcould not complete and #35 had to be pushed with--no-verify. It is now pinned in.devcontainer/mise.tomlat 1.7.11 (matchingci.yml), resolved per-architecture and attestation-verified by mise.verify_toolswas checking the wrong property. It logged$("$cmd" --version || echo installed), which swallowed the failure — it would have called the broken binary installed. Being on thePATHis not what matters; being runnable is. It now fails on a non-zero--version, andactionlintandshellcheckjoin the verified list sincetask checkruns both. Confirmed against a deliberately unrunnable binary.Taskfile.ymlalso dropsLEFTHOOK_VERSIONandACTIONLINT_VERSION, both dead — the latter a declared intent with nothing enforcing it.Dependabot #33 and #34 are cherry-picked with
-xand their original authorship. One loose end they left: the biome bump changed the CLI but not the$schemaURL intools/biome.json, so everybiome cirun reported a version mismatch.biome migratecloses it in a separate commit.Compatibility
Three new
semanticcodes and onestructuralnarrowing.No new
v<N>directory, and the reason is that v1 has never been published.git tag -lis empty and.github/release-please/manifest.jsonreads0.0.0for all three families, so component §3's guarantee has no releasedv1.0.0to run from. This is #35's argument and #31's before it, with the same expiry: it dies the moment release PRs #1, #2 or #3 merge. This needs to land before them.Corpus impact — checked, not assumed
Only two authored
parametersblocks exist in the entire repository, and neither is affected:examples/web-and-database.yaml— validated with no item root, so no semantic rule reaches it.structural/005-nested-unknown-field— acase.yaml, so no item root either.Every other occurrence in the corpus is the empty literal
parameters: {}, which takes the derivation path and is untouched.semantic-009-conflicting-input-schemais empty too, so gating the merge leaves it green.One thing to note downstream: the platform runs this corpus as a contract test (
tests/contract/spec_conformance/test_blueprint_corpus.py) with aDivergencePinmechanism, so these three codes will surface there as new divergences until it implements them — relevant to open issue #24.Checklist
task checkpasses locally — includingcheck:workflow, which is the point of the tooling halfschemas/dist/regenerated withtask bundleand committed (never edited by hand)clausespec.md— schemadescriptions are explanatory, not normativegit commit -s)7 new fixtures, 107 → 114 cases, 0 skipped. No new
UNCOVEREDentry — every code here issemanticorstructuraland therefore testable, unlike #35's capability code. Each negative fixture was run against the pre-change tree and confirmed to report "expected to fail but validated cleanly";semantic-020was confirmed to fail in the opposite direction.Blueprint
spec.mdnow carries noTODOsections at all. Onlylistingstill does.🤖 Generated with Claude Code