feat(intake): non-binary archive lane and provisional evidence tier - #103
feat(intake): non-binary archive lane and provisional evidence tier#103tonythethompson wants to merge 4 commits into
Conversation
Add --provisional / --deferral-reason so the binary intake lane can ship a spec for a plugin that builds but cannot run lifecycle-prove in CI (the nu_plugin_bigquery case: it needs Google credentials). A provisional spec carries evidence_tier + a non-blank deferral_reason and omits verified_with entirely, because numan-registry's add-package.py aborts when --provisional is paired with a spec that merely contains the key, and its validate_spec only waives the lifecycle-evidence check when the key is absent. build_spec raises ValueError (never SystemExit) for a blank reason, a reason without --provisional, and --provisional on an entry that already has verified_with evidence, so main() reports them as FAIL and returns 1. Non-provisional output is unchanged byte-for-byte. Refs #86 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modules, scripts, and completions need no compilation, so they take an archive lane instead of the cross-compile matrix. scripts/intake_archive.py resolves an upstream ref to its immutable commit, shallow-clones it, verifies the declared entry file, builds a deterministic .tar.gz with package_plugin.py's parameters, emits an add-package-compatible archive spec with an inline sha256, and records re-intake provenance in manifest-archives.json. .github/workflows/intake-archive.yml drives the lane on manual dispatch and publishes through the existing release transaction (ensure_release_absent.py plus release_transaction.py claim/upload/finalize/cleanup), so the script itself publishes nothing and stays hermetically testable. workflow_dispatch accepts at most 10 inputs, so the dispatch surface carries owner/name as `package` and activation kind/import as `activation`, treats a non-blank `deferral_reason` as provisional intake, and always derives the version from the ref. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sing Review response for the non-binary archive lane: - the publish job re-hashes the collected artifact against the digest the archive job computed and refuses a dist that is not exactly the one expected asset, the gate build.yml has via verify_packaged_assets - correct the inline artifact.sha256 rationale in the module docstring and docs/roadmap.md: add-package.py recomputes the hash from the download and ignores the spec value - record why an activation always lands provisional: lifecycle evidence can only come from proving the published asset, which does not exist yet - reject an empty owner/name half in the workflow, and validate owner, name and version in the script before they reach a release tag or filename - treat a trailing-colon activation as having no import mode instead of failing argparse's choices with a usage dump - build the archive beside its target and rename, so a failed write cannot leave a partial file that blocks the retry - cover archive member mode normalization, partial-archive cleanup, the --repo slug path, and upload-by-claimed-release-id for the new lane - note that gen_spec.py's provisional flags have no build.yml input by design, and where the evidence tier actually gets written Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The gate test asserted the digest check but not the mapfile / -ne 1 block, so deleting the count guard left the suite green while release_transaction.py upload would publish any extra file that reached dist. Scope the assertions to the gate step and pin the enumeration, the count check and the name equality. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
We've triggered an ultrareview automatically — This PR adds a new archive intake lane and a release-publishing workflow with elevated permissions, plus provisional spec logic; a subtle bug could publish a tampered artifact or produce an invalid registry entry, so a deeper review is warranted.. I'll post findings when complete. An ultrareview is cubic's deepest review, catching hard-to-find bugs in the most critical PRs. It runs a longer, multi-pass analysis using cubic's most capable review models, and typically takes around 30 minutes. It consumes your team's reviewed-lines allowance at 3× the standard rate. Automated ultrareviews are disabled by default. We triggered this run as part of your trial. Want cubic to do this for every high-risk PR? Enable auto-ultrareview in your settings. |
There was a problem hiding this comment.
Ultrareview completed in 10m 36s
8 issues found across 9 files
Confidence score: 2/5
scripts/intake_archive.pyaccepts malformed SemVer tags and explicit versions, allowing invalid releases to be created; parse both derived and explicit versions strictly before release creation.scripts/intake_archive.pycan emit activation metadata for invalid module kinds, non-.nuentries, or entries excluded from the archive, causing downstream installation to abort; validate the module and entry contracts before publishing.scripts/intake_archive.pycopies--nu-versionconstraints without validation, so invalid ranges may be treated as matching and produce packages with incorrect compatibility claims; reject unrecognized constraint syntax..github/workflows/intake-archive.ymlmishandles malformed activation values, can execute unsafedeferral_reasoncontent, and can overwrite concurrent intake results with stale manifest snapshots; strictly parse and shell/Markdown-escape handoff data, then merge concurrent manifest updates safely.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="scripts/intake_archive.py">
<violation number="1" location="scripts/intake_archive.py:209">
P2: When `--entry` is `.git/HEAD`, this check succeeds even though `sorted_files` excludes `.git`; the emitted archive lacks the declared entry and the downstream installer aborts at numan-cli/numan/src/install/transaction.rs:323-330. Validate the entry against archiveable, normalized paths before emitting the spec.</violation>
<violation number="2" location="scripts/intake_archive.py:293">
P1: Tags such as `v1.2.3-01` or `v1.2.3+foo..bar` match this regex and pass `VERSION_RE`, but they are invalid SemVer. Parse both derived and explicit versions strictly before creating the release, because the resulting spec can be rejected or make the signed index unreadable at numan-cli/numan/src/core/package.rs:103-109.</violation>
<violation number="3" location="scripts/intake_archive.py:326">
P2: When activation is requested for a script, an unknown kind, or a non-`.nu` entry, this function still emits activation metadata. Validate `type == module`, `kind == nu-module`, and the `.nu` entry contract before publishing, because the client cannot activate those combinations at numan-cli/numan/src/install/transaction.rs:374-382.</violation>
<violation number="4" location="scripts/intake_archive.py:409">
P2: `--nu-version` is copied verbatim, so `bad range` becomes a valid-looking spec. The client ignores unrecognized constraint tokens and returns true at numan-cli/numan/src/core/nu_version.rs:76-123, making the package appear compatible with every Nu; validate the accepted range grammar before emission.</violation>
<violation number="5" location="scripts/intake_archive.py:418">
P2: When `--activation-import` is supplied without `--activation-kind`, validation accepts it and this guard silently emits an install-only spec. Reject an import mode without its activation kind.</violation>
</file>
<file name=".github/workflows/intake-archive.yml">
<violation number="1" location=".github/workflows/intake-archive.yml:117">
P2: When `activation` contains more than one colon, this split silently drops or ignores a segment instead of rejecting malformed `kind[:import]` input. For example, `nu-module:all:` emits default `module` metadata instead of the requested `all`; reject multiple colons before splitting.</violation>
<violation number="2" location=".github/workflows/intake-archive.yml:304">
P2: Two intake runs for different packages can run concurrently, but each handoff contains a full stale `manifest-archives.json` snapshot. Committing one artifact over the file drops records from the other run; merge the artifact into the current provenance file instead of replacing it.</violation>
<violation number="3" location=".github/workflows/intake-archive.yml:307">
P2: When `deferral_reason` contains quotes, backticks, `$()` or a newline, this copy-paste handoff can break the command or execute command substitutions. Shell-escape the value and Markdown-escape the summary, or omit raw user text from the command.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with Cursor
Re-trigger cubic
| anything else falls back to the 0.1.0-<short-sha> convention the registry | ||
| already uses for branch-pinned script and completion entries. | ||
| """ | ||
| match = re.fullmatch(r"v?(\d+\.\d+\.\d+(?:[-+].+)?)", ref) |
There was a problem hiding this comment.
P1: Tags such as v1.2.3-01 or v1.2.3+foo..bar match this regex and pass VERSION_RE, but they are invalid SemVer. Parse both derived and explicit versions strictly before creating the release, because the resulting spec can be rejected or make the signed index unreadable at numan-cli/numan/src/core/package.rs:103-109.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/intake_archive.py, line 293:
<comment>Tags such as `v1.2.3-01` or `v1.2.3+foo..bar` match this regex and pass `VERSION_RE`, but they are invalid SemVer. Parse both derived and explicit versions strictly before creating the release, because the resulting spec can be rejected or make the signed index unreadable at numan-cli/numan/src/core/package.rs:103-109.</comment>
<file context>
@@ -0,0 +1,604 @@
+ anything else falls back to the 0.1.0-<short-sha> convention the registry
+ already uses for branch-pinned script and completion entries.
+ """
+ match = re.fullmatch(r"v?(\d+\.\d+\.\d+(?:[-+].+)?)", ref)
+ if match:
+ return match.group(1)
</file context>
| "sha256": sha256, | ||
| }, | ||
| } | ||
| if activation_kind: |
There was a problem hiding this comment.
P2: When --activation-import is supplied without --activation-kind, validation accepts it and this guard silently emits an install-only spec. Reject an import mode without its activation kind.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/intake_archive.py, line 418:
<comment>When `--activation-import` is supplied without `--activation-kind`, validation accepts it and this guard silently emits an install-only spec. Reject an import mode without its activation kind.</comment>
<file context>
@@ -0,0 +1,604 @@
+ "sha256": sha256,
+ },
+ }
+ if activation_kind:
+ activation = {"kind": activation_kind}
+ if activation_import:
</file context>
| reason is given without provisional intake, or if a `mod.nu` entry | ||
| is activated with import mode 'module'. | ||
| """ | ||
| if activation_kind and not provisional: |
There was a problem hiding this comment.
P2: When activation is requested for a script, an unknown kind, or a non-.nu entry, this function still emits activation metadata. Validate type == module, kind == nu-module, and the .nu entry contract before publishing, because the client cannot activate those combinations at numan-cli/numan/src/install/transaction.rs:374-382.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/intake_archive.py, line 326:
<comment>When activation is requested for a script, an unknown kind, or a non-`.nu` entry, this function still emits activation metadata. Validate `type == module`, `kind == nu-module`, and the `.nu` entry contract before publishing, because the client cannot activate those combinations at numan-cli/numan/src/install/transaction.rs:374-382.</comment>
<file context>
@@ -0,0 +1,604 @@
+ reason is given without provisional intake, or if a `mod.nu` entry
+ is activated with import mode 'module'.
+ """
+ if activation_kind and not provisional:
+ raise ValueError(
+ "an activation requires provisional intake (no lifecycle evidence "
</file context>
| "type": pkg_type, | ||
| "tags": tags, | ||
| "version": version, | ||
| "nu_version": nu_version, |
There was a problem hiding this comment.
P2: --nu-version is copied verbatim, so bad range becomes a valid-looking spec. The client ignores unrecognized constraint tokens and returns true at numan-cli/numan/src/core/nu_version.rs:76-123, making the package appear compatible with every Nu; validate the accepted range grammar before emission.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/intake_archive.py, line 409:
<comment>`--nu-version` is copied verbatim, so `bad range` becomes a valid-looking spec. The client ignores unrecognized constraint tokens and returns true at numan-cli/numan/src/core/nu_version.rs:76-123, making the package appear compatible with every Nu; validate the accepted range grammar before emission.</comment>
<file context>
@@ -0,0 +1,604 @@
+ "type": pkg_type,
+ "tags": tags,
+ "version": version,
+ "nu_version": nu_version,
+ **evidence,
+ "artifact": {
</file context>
| resolved.relative_to(src_dir.resolve()) | ||
| except ValueError as exc: | ||
| raise ValueError(f"entry path escapes checkout: {entry}") from exc | ||
| if not resolved.is_file(): |
There was a problem hiding this comment.
P2: When --entry is .git/HEAD, this check succeeds even though sorted_files excludes .git; the emitted archive lacks the declared entry and the downstream installer aborts at numan-cli/numan/src/install/transaction.rs:323-330. Validate the entry against archiveable, normalized paths before emitting the spec.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/intake_archive.py, line 209:
<comment>When `--entry` is `.git/HEAD`, this check succeeds even though `sorted_files` excludes `.git`; the emitted archive lacks the declared entry and the downstream installer aborts at numan-cli/numan/src/install/transaction.rs:323-330. Validate the entry against archiveable, normalized paths before emitting the spec.</comment>
<file context>
@@ -0,0 +1,604 @@
+ resolved.relative_to(src_dir.resolve())
+ except ValueError as exc:
+ raise ValueError(f"entry path escapes checkout: {entry}") from exc
+ if not resolved.is_file():
+ raise ValueError(f"entry file not found in checkout: {entry}")
+ return resolved
</file context>
| echo "- Commit the \`manifest-archives\` artifact over \`manifest-archives.json\` to keep re-intake provenance; this run pushes no commit." | ||
| if [ -n "$DEFERRAL_REASON" ]; then | ||
| echo "- provisional intake, deferral reason: $DEFERRAL_REASON" | ||
| echo "- next, in numan-registry: \`python scripts/add-package.py --spec $spec --write --provisional --deferral-reason \"$DEFERRAL_REASON\"\`" |
There was a problem hiding this comment.
P2: When deferral_reason contains quotes, backticks, $() or a newline, this copy-paste handoff can break the command or execute command substitutions. Shell-escape the value and Markdown-escape the summary, or omit raw user text from the command.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/intake-archive.yml, line 307:
<comment>When `deferral_reason` contains quotes, backticks, `$()` or a newline, this copy-paste handoff can break the command or execute command substitutions. Shell-escape the value and Markdown-escape the summary, or omit raw user text from the command.</comment>
<file context>
@@ -0,0 +1,333 @@
+ echo "- Commit the \`manifest-archives\` artifact over \`manifest-archives.json\` to keep re-intake provenance; this run pushes no commit."
+ if [ -n "$DEFERRAL_REASON" ]; then
+ echo "- provisional intake, deferral reason: $DEFERRAL_REASON"
+ echo "- next, in numan-registry: \`python scripts/add-package.py --spec $spec --write --provisional --deferral-reason \"$DEFERRAL_REASON\"\`"
+ else
+ echo "- provisional intake: no"
</file context>
| echo "- release tag: \`$TAG\`" | ||
| echo "- upstream commit: \`$RESOLVED_SHA\`" | ||
| echo "- archive sha256: \`$ARCHIVE_SHA256\`" | ||
| echo "- Commit the \`manifest-archives\` artifact over \`manifest-archives.json\` to keep re-intake provenance; this run pushes no commit." |
There was a problem hiding this comment.
P2: Two intake runs for different packages can run concurrently, but each handoff contains a full stale manifest-archives.json snapshot. Committing one artifact over the file drops records from the other run; merge the artifact into the current provenance file instead of replacing it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/intake-archive.yml, line 304:
<comment>Two intake runs for different packages can run concurrently, but each handoff contains a full stale `manifest-archives.json` snapshot. Committing one artifact over the file drops records from the other run; merge the artifact into the current provenance file instead of replacing it.</comment>
<file context>
@@ -0,0 +1,333 @@
+ echo "- release tag: \`$TAG\`"
+ echo "- upstream commit: \`$RESOLVED_SHA\`"
+ echo "- archive sha256: \`$ARCHIVE_SHA256\`"
+ echo "- Commit the \`manifest-archives\` artifact over \`manifest-archives.json\` to keep re-intake provenance; this run pushes no commit."
+ if [ -n "$DEFERRAL_REASON" ]; then
+ echo "- provisional intake, deferral reason: $DEFERRAL_REASON"
</file context>
| flags=() | ||
| if [ -n "$ACTIVATION" ]; then | ||
| activation_kind="${ACTIVATION%%:*}" | ||
| activation_import="${ACTIVATION##*:}" |
There was a problem hiding this comment.
P2: When activation contains more than one colon, this split silently drops or ignores a segment instead of rejecting malformed kind[:import] input. For example, nu-module:all: emits default module metadata instead of the requested all; reject multiple colons before splitting.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/intake-archive.yml, line 117:
<comment>When `activation` contains more than one colon, this split silently drops or ignores a segment instead of rejecting malformed `kind[:import]` input. For example, `nu-module:all:` emits default `module` metadata instead of the requested `all`; reject multiple colons before splitting.</comment>
<file context>
@@ -0,0 +1,333 @@
+ flags=()
+ if [ -n "$ACTIVATION" ]; then
+ activation_kind="${ACTIVATION%%:*}"
+ activation_import="${ACTIVATION##*:}"
+ if [ -z "$activation_kind" ]; then
+ echo "FAIL: activation must be kind[:import]: $ACTIVATION" >&2
</file context>
| activation_import="${ACTIVATION##*:}" | |
| if [[ "$ACTIVATION" == *:*:* ]]; then | |
| echo "FAIL: activation must be kind[:import]: $ACTIVATION" >&2 | |
| exit 1 | |
| fi | |
| activation_import="${ACTIVATION##*:}" |
Adds the P2 non-binary archive intake lane (#85) and the P6 provisional evidence tier (#86).
#86 — provisional evidence tier in
gen_spec.pybuild_specgained keyword-onlyprovisional/deferral_reason, surfaced as--provisional/--deferral-reason. A provisional spec omitsverified_withentirely and emitsevidence_tier: "provisional"plus a strippeddeferral_reasonin its place — numan-registry'sadd-package.pyaborts when the key is merely present under--provisional, and itsvalidate_speconly waives the lifecycle-evidence check when the key is absent.Three guards reject a blank or missing reason, a reason passed without
--provisional, and--provisionalagainst a manifest entry that already has lifecycle evidence.#85 — non-binary archive intake lane
New
scripts/intake_archive.pyresolves a ref to its immutable 40-char SHA, shallow-clones, verifies the declared entry file, builds a deterministic.tar.gzwithpackage_plugin.py's parameters (sorted entries, fixed mtime, gzip mtime=0), emits anartifact.kind: archivespec, upsertsmanifest-archives.json, and prints anARCHIVEDTSV handoff line. It publishes nothing, so it stays hermetically testable.New
.github/workflows/intake-archive.ymlpublishes through the repo's auditedensure_release_absent.py+release_transaction.pyclaim → upload → finalize flow (cleanup on failure) rather than an inlinegh release create. Between the two jobs, a gate re-hashes the downloaded artifact against the digest the archive job computed and refuses to proceed unlessdistholds exactly the one expected file — nothing is claimed or published when it fires.Top-level
permissions: contents: read; only the publish job escalates tocontents: write.Deviation from the plan
GitHub caps
workflow_dispatchat 10 inputs and CI's actionlint step failed with the planned 14. The dispatch surface is nowgit_url, ref, entry, package(owner/name, split in bash),type, description, tags, nu_version, activation(kind[:import]),deferral_reason(non-blank implies provisional). Theversionoverride is no longer a dispatch input and always derives from the ref; the script still accepts--owner/--name/--version/--provisional/--activation-kind/--activation-importfor local runs.Tested
python -m unittest discover -s scripts -p "test_*.py"→ 230 tests, OK (skipped=1: the archive-member exec-bit test, which skips on Windows and runs on CI's ubuntu-latest)python -m compileall -q scriptsexit 0scripts/format_json.py --checkandscripts/check_repo_consistency.pyOKactionlint1.7.7 exit 0;shellcheck0.10.0 clean over everyrunbody, including at-S stylewith no exclusionsdist, extra file, nested archive) — all fail closedTwo passes of semantic review; approved at v2. The first pass caught a blocking gap — the publish job did not re-hash the downloaded artifact — which
5f1874bfixed.Not runnable locally:
roadmap-drift.yml(its checker lives in thenumanrepo, so only CI can confirm the roadmap edit) andvalidate_manifest.py --verify-upstream(needs per-plugin network git; unaffected by this change).Known follow-ups
gen_spec.py --provisionalhas no CI entry point.build.ymlis a single-input matrix over many plugins while the tier is per package, so the natural home is a per-entrymanifest.jsonfield — out of scope here. The rationale and the manual procedure for the motivatingnu_plugin_bigqueryintake are recorded indocs/roadmap.md.nu-module:bogus) exits 2 with an argparse usage dump instead of the repo'sFAIL:line, and a multi-colon input drops its middle segment. Both fail closed.Closes #85
Closes #86
Summary by cubic
Adds a manual-dispatch workflow that archives non-binary packages (modules, scripts, completions) from an upstream commit and publishes them as release assets, plus a provisional evidence tier in
gen_spec.pyfor packages that build but can't run lifecycle-prove in CI.Non-binary archive intake
scripts/intake_archive.pyresolves a ref to its commit SHA, shallow-clones, verifies the entry file, and builds a deterministic.tar.gz; it publishes nothing itself..github/workflows/intake-archive.ymlpublishes through the existingensure_release_absent.pyandrelease_transaction.pyclaim→upload→finalize flow, cleaning up on failure.distholds exactly that one file.contents: write.workflow_dispatchat 10 inputs, so the dispatch surface is collapsed to 9 fields;versionalways derives from the ref.manifest-archives.json, uploaded as an artifact for a maintainer to commit (the workflow pushes no commit).Provisional evidence tier
gen_spec.pygains--provisional/--deferral-reason; provisional specs omitverified_withand emitevidence_tier: "provisional"with a strippeddeferral_reason.--provisional, and--provisionalon an entry that already has lifecycle evidence.Closes #85 and #86.
Written for commit c6c700b. Summary will update on new commits.