Skip to content
Merged
41 changes: 34 additions & 7 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -4069,9 +4069,10 @@ check.
Release builds generate their manual and PowerShell help explicitly with
`cargo-orthohelp`, rather than consuming the ordinary-build help artefacts from
`build.rs`. The metadata root is `netsuke::cli::ReleaseHelpCli`, the sole
permitted composition site for release help. `CliConfig` supplies only layered
configuration fields; `Cli::command()` supplies parser-only flags such as
`-C/--directory` and `--config`, plus documented subcommands, including
permitted composition site for release help. `CliConfig` supplies the layered
configuration fields and owns each configuration-derived value placeholder
through `cli(value_name = "...")`; `Cli::command()` supplies parser-only flags
such as `-C/--directory` and `--config`, plus documented subcommands, including
`help targets`. The adapter projects existing CLI Fluent keys onto published
configuration fields and adds parser-only help metadata without adding an
environment or file source. It omits the structural `cmds` container. Keep
Expand All @@ -4092,10 +4093,11 @@ every derived boolean as `--flag[=BOOL]`, because its *generated* clap parser
accepts an optional value; Netsuke declares its flags by hand in
`src/cli/command.rs` and rejects that spelling, so the adapter reduces boolean
fields back to presence-only switches. Two tests hold both projections:
`release_help_identifiers_are_netsuke_declared_not_generated` and
`release_help_cli_value_shapes_match_the_parser` in
`src/cli/release_help/metadata_tests.rs`. A new derived field, or a new
upstream metadata change, is expected to reach one of them.
identifier provenance in `src/cli/release_help/metadata_tests/mod.rs`
(`release_help_identifiers_are_netsuke_declared_not_generated`), and
value-shape parity in `src/cli/release_help/metadata_tests/value_shapes.rs`
(`release_help_cli_value_shapes_match_the_parser`). A new derived field, or a
new upstream metadata change, is expected to reach one of them.

The root synopsis is cleared for the same reason. `ortho_config` 0.10.0's
derive defaults `synopsis_id` to a generated `<app>-usage` identifier that no
Expand All @@ -4111,6 +4113,31 @@ catalogue. The identifier guard collects `synopsis_id` alongside `help_id` and
`about_id`, and the release-help metadata snapshot pins the published value, so
a future derive default cannot reintroduce the placeholder silently.

Value placeholders follow the same ownership split. A configuration-derived
option declares its placeholder in `CliConfig` with `cli(value_name = "...")`;
without that declaration the generator falls back to a machine type such as
`UTF8PATHBUF`, `USIZE`, or `LIST`, and publishes it in the manual page and
`PowerShell` help. `src/cli/release_help/parser_fields.rs` remains responsible
only for parser-only selectors, which have no configuration source from which
to derive a placeholder. The parity test compares every value-taking option's
`cli.value_name` against the first value name `Cli::command()` declares, so
adding a configuration-derived value-taking option requires matching
declarations in `src/cli/config/mod.rs` and `src/cli/command.rs`. A parser-only
selector needs only the parser declaration, because `parser_fields.rs` derives
its placeholder from that declaration. A placeholder that disagrees with the
parser fails the test rather than reaching a release artefact. The release-help
script tests render every declared value name through the fake generator and
assert the placeholder appears in both the manual page and the PowerShell MAML
output, and the metadata snapshot records each field's `value_name`, so a lost
declaration fails the suite before it can reach a release.

The release lane adds a guard on the artefacts themselves:
`scripts/generate-release-help.sh` rejects a generated manual page or
PowerShell help that publishes one of the generator's machine type names
(`UTF8PATHBUF`, `USIZE`, `LIST`, `INT`, `STRING`) as a placeholder, so the real
renderer is checked on what ships, not only under the fake generator the tests
drive.

Release automation delegates installation to
[`install_orthohelp.py`](../scripts/install_orthohelp.py), with the pinned
version supplied as `INPUT_VERSION`. The helper probes the cached binary and,
Expand Down
8 changes: 8 additions & 0 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,14 @@ only. Translated copy reaches users through the running binary, which embeds
every catalogue. The generated help describes the complete public CLI, including
`-C/--directory`, `--config`, and the documented command tree.

Generated help uses the same readable value placeholders as `--help`.
Configuration-derived options show `-f, --file <FILE>`, `-j, --jobs <N>`,
`--locale <LOCALE>`, the list options' `<SCHEME>`, `<VAR>`, `<HOST>`, and
`<TARGET>` names, and the manifest budget options' `<FUEL>`, `<BYTES>`, and
`<COUNT>` names, rather than internal configuration type names. Parser-only
selectors such as `-C/--directory` and `--config` show the placeholders the
command parser declares.

### Anchor a project with `--directory`

`--directory` changes manifest lookup, project configuration discovery and
Expand Down
44 changes: 44 additions & 0 deletions scripts/generate-release-help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,46 @@ file_size_bytes() {
wc -c <"$path" | tr -d '[:space:]'
}

# Machine type names `cargo-orthohelp` substitutes into a placeholder when the
# metadata declares no value name. Publishing one is the regression this lane
# exists to catch, and only the real renderer can prove the metadata reached it.
machine_value_names=(STRING INT LIST USIZE UTF8PATHBUF)

# Fail when a generated artefact publishes a machine type name as a placeholder.
#
# The checks match the markup that marks a placeholder, not the bare word, so
# ordinary prose mentioning a string or an integer does not trip them.
reject_machine_value_placeholders() {
local path="$1"
local format="$2"
local name
local published=()

for name in "${machine_value_names[@]}"; do
case "$format" in
man)
if grep -qF "\\fI${name}\\fP" "$path"; then
published+=("$name")
fi
;;
ps)
if grep -qF "<command:parameterValue required=\"true\">${name}</command:parameterValue>" "$path"; then
published+=("$name")
fi
;;
*)
echo "::error title=Release help placeholder::$(annotation_context "$format") unknown format ${format}" >&2
exit 1
;;
esac
done

if (( ${#published[@]} > 0 )); then
echo "::error title=Release help placeholder::$(annotation_context "$format") machine value names published (${published[*]}) in ${path}" >&2
exit 1
fi
}

require_cargo_orthohelp() {
if ! command -v cargo-orthohelp >/dev/null 2>&1; then
echo "::error title=cargo-orthohelp missing::$(annotation_context setup) cargo-orthohelp was not found on PATH" >&2
Expand Down Expand Up @@ -171,6 +211,7 @@ run_cargo_orthohelp "man" \
--man-date "$man_date"

require_file "$out_dir/man/man1/${bin_name}.1" "manual page was not generated"
reject_machine_value_placeholders "$out_dir/man/man1/${bin_name}.1" "man"

if target_is_windows "$target"; then
run_cargo_orthohelp "ps" \
Expand All @@ -189,4 +230,7 @@ if target_is_windows "$target"; then
"PowerShell MAML help was not generated"
require_file "$out_dir/powershell/$module_name/en-US/about_$module_name.help.txt" \
"PowerShell about help was not generated"
reject_machine_value_placeholders \
"$out_dir/powershell/$module_name/en-US/$module_name-help.xml" \
"ps"
fi
48 changes: 34 additions & 14 deletions src/cli/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,41 +198,43 @@ pub struct CommandConfigs {
#[ortho_config(prefix = "NETSUKE", post_merge_hook)]
pub struct CliConfig {
/// Path to the Netsuke manifest file to use.
#[ortho_config(default = default_manifest_path())]
#[ortho_config(default = default_manifest_path(), cli(value_name = "FILE"))]
pub file: Utf8PathBuf,

/// Set the number of parallel build jobs.
#[ortho_config(cli(value_name = "N"))]
pub jobs: Option<usize>,

/// Enable verbose diagnostic logging and completion timing summaries.
#[ortho_config(default = false)]
pub verbose: bool,

/// Locale tag for CLI copy (for example: en-US, es-ES).
#[ortho_config(cli(value_name = "LOCALE"))]
pub locale: Option<String>,

/// Additional URL schemes allowed for the `fetch` helper.
#[ortho_config(merge_strategy = "append")]
#[ortho_config(merge_strategy = "append", cli(value_name = "SCHEME"))]
#[serde(default)]
pub fetch_allow_scheme: Vec<String>,

/// Environment variables permitted for the manifest `env()` helper.
#[ortho_config(merge_strategy = "append")]
#[ortho_config(merge_strategy = "append", cli(value_name = "VAR"))]
#[serde(default)]
pub env_allow_var: Vec<String>,

/// Environment variables always blocked for the manifest `env()` helper.
#[ortho_config(merge_strategy = "append")]
#[ortho_config(merge_strategy = "append", cli(value_name = "VAR"))]
#[serde(default)]
pub env_block_var: Vec<String>,

/// Hostnames permitted when default deny is enabled.
#[ortho_config(merge_strategy = "append")]
#[ortho_config(merge_strategy = "append", cli(value_name = "HOST"))]
#[serde(default)]
pub fetch_allow_host: Vec<HostPattern>,

/// Hostnames that are always blocked.
#[ortho_config(merge_strategy = "append")]
#[ortho_config(merge_strategy = "append", cli(value_name = "HOST"))]
#[serde(default)]
pub fetch_block_host: Vec<HostPattern>,

Expand All @@ -245,31 +247,49 @@ pub struct CliConfig {
pub trust_project_fetch_policy: bool,

/// Maximum `MiniJinja` instructions allocated to one manifest evaluation.
#[ortho_config(default = DEFAULT_MANIFEST_EVALUATION_FUEL)]
#[ortho_config(
default = DEFAULT_MANIFEST_EVALUATION_FUEL,
cli(value_name = "FUEL")
)]
pub manifest_evaluation_fuel: u64,

/// Maximum `MiniJinja` instructions allocated across one manifest.
#[ortho_config(default = DEFAULT_MANIFEST_FUEL)]
#[ortho_config(default = DEFAULT_MANIFEST_FUEL, cli(value_name = "FUEL"))]
pub manifest_fuel: u64,

/// Maximum bytes emitted by one rendered manifest value.
#[ortho_config(default = DEFAULT_MANIFEST_RENDERED_VALUE_BYTES)]
#[ortho_config(
default = DEFAULT_MANIFEST_RENDERED_VALUE_BYTES,
cli(value_name = "BYTES")
)]
pub manifest_rendered_value_bytes: usize,

/// Maximum aggregate bytes emitted across all rendered manifest values.
#[ortho_config(default = DEFAULT_MANIFEST_RENDERED_BYTES)]
#[ortho_config(
default = DEFAULT_MANIFEST_RENDERED_BYTES,
cli(value_name = "BYTES")
)]
pub manifest_rendered_manifest_bytes: usize,

/// Maximum template and macro-import source bytes consumed per manifest.
#[ortho_config(default = DEFAULT_MANIFEST_SOURCE_BYTES)]
#[ortho_config(
default = DEFAULT_MANIFEST_SOURCE_BYTES,
cli(value_name = "BYTES")
)]
pub manifest_source_bytes: usize,

/// Maximum values consumed from each manifest `foreach` iterator.
#[ortho_config(default = DEFAULT_MANIFEST_FOREACH_CARDINALITY)]
#[ortho_config(
default = DEFAULT_MANIFEST_FOREACH_CARDINALITY,
cli(value_name = "COUNT")
)]
pub manifest_foreach_cardinality: usize,

/// Maximum expanded target and action entries per manifest.
#[ortho_config(default = DEFAULT_MANIFEST_EXPANDED_ENTRIES)]
#[ortho_config(
default = DEFAULT_MANIFEST_EXPANDED_ENTRIES,
cli(value_name = "COUNT")
)]
pub manifest_expanded_entries: usize,

/// Emit machine-readable JSON output.
Expand Down Expand Up @@ -297,7 +317,7 @@ pub struct CliConfig {
pub accessibility: AccessibilityPolicy,

/// Compatibility alias for default build targets at the config root.
#[ortho_config(merge_strategy = "append")]
#[ortho_config(merge_strategy = "append", cli(value_name = "TARGET"))]
#[serde(default)]
pub default_targets: Vec<String>,

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
//! Unit tests for the complete release-help metadata contract.
//!
//! The tests cover the configuration-help projection, the parser-only field
//! and value-shape derivation, and the identifiers carried by the composed
//! root and immediate subcommand documentation metadata.
//! The tests cover the configuration-help projection, parser-only field and
//! value-shape derivation, localization, and the identifiers carried by the
//! composed root and immediate subcommand documentation metadata. Value-shape
//! parity with the parser lives in [`value_shapes`].

use super::*;
use anyhow::{Context, Result, ensure};
use clap::Command;
use rstest::rstest;

mod value_shapes;

use super::parser_fields::documented_clap_parser_field;

struct ParserOnlyFieldExpectation {
Expand Down Expand Up @@ -132,95 +135,6 @@ fn parser_only_metadata_requires_the_parser_argument(#[case] argument_id: &str)
Ok(())
}

/// Verify that documented CLI value shapes match the parser users actually run.
///
/// `ortho_config` 0.10.0 documents every derived boolean as `--flag[=BOOL]`,
/// because its generated parser accepts an optional value. Netsuke declares its
/// flags by hand and rejects that spelling, so release help normalizes boolean
/// fields back to presence-only switches. Comparing against `Cli::command()`
/// makes the parity a standing contract: a future metadata drift fails here
/// instead of shipping a manual page the binary contradicts.
///
/// Value *names* are deliberately not compared. Netsuke sets no
/// `cli_value_name`, so the generated metadata has always reported `None` while
/// the handwritten parser supplies `FILE`, `N`, and friends; that gap predates
/// the v0.10.0 migration and is not this contract's subject.
#[test]
fn release_help_cli_value_shapes_match_the_parser() -> Result<()> {
let metadata = ReleaseHelpCli::try_get_doc_metadata()?;
let command = Cli::command();
let mut compared = Vec::new();

for field in &metadata.fields {
let Some(documentation) = field.cli.as_ref() else {
continue;
};
let argument = command
.get_arguments()
.find(|argument| argument.get_id().as_str() == field.name)
.with_context(|| {
format!(
"release help documents {}, but the parser accepts no such \
argument; the manual page would advertise a flag this \
binary rejects",
field.name,
)
})?;

let takes_value = argument.get_action().takes_values();
ensure!(
documentation.takes_value == takes_value,
"release help should document the value shape {name} parses: \
documented {documented}, parser {parsed}",
name = field.name,
documented = documentation.takes_value,
parsed = takes_value,
);
ensure!(
!documentation.value_optional,
"release help must not offer the optional-value spelling for {}",
field.name
);
if takes_value {
ensure!(
documentation.possible_values
== argument
.get_possible_values()
.iter()
.map(|value| value.get_name().to_owned())
.collect::<Vec<_>>(),
"release help should document the accepted values for {}",
field.name
);
} else {
// A presence-only switch parses no value at all. Clap still records
// `possible_values` for `SetTrue` actions, so copying them would
// describe a value surface no invocation can reach.
ensure!(
documentation.value_name.is_none() && documentation.possible_values.is_empty(),
"release help should describe {} as a switch with no value",
field.name
);
}
compared.push(field.name.as_str());
}

// The four boolean flags that 0.10.0 first documented as `--flag[=BOOL]`
// are the reason this contract exists, so their absence proves nothing.
for expected in [
"verbose",
"fetch_default_deny",
"trust_project_fetch_policy",
"json",
] {
ensure!(
compared.contains(&expected),
"release help should compare {expected} against the parser: {compared:?}"
);
}
Ok(())
}

/// Verify that release help includes every parser-only selector.
#[test]
fn release_help_metadata_composes_every_parser_only_selector() -> Result<()> {
Expand Down
Loading
Loading