diff --git a/CHANGELOG.md b/CHANGELOG.md index d5dbbb5..ce1c03a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and versions are tracked in the repo-root `VERSION` file. - Added a repository-owned v2 release guard that permits only the planned alpha, beta, release-candidate, and GA identifiers while locking publication until the verified-artifact and pre-GA release-candidate gates are complete. +- Added the versioned `base_api_manifest.yaml` module/API contract, its + Bash-native validator and generators, and manifest-driven module/test/artifact + discovery for repository validation. ### Security @@ -59,8 +62,8 @@ and versions are tracked in the repo-root `VERSION` file. `base_init` lifecycle API. Wrapper flags now return through a caller-owned array without hidden positional-parameter mutation; launchers, examples, and companion-library tests initialize explicitly. -- Namespaced the v2 public API, runtime globals, environment controls, load - guards, and internal symbols under the `base_bash_libs_` contract. Legacy +- Namespaced the v2 public API under the coherent `base_` contract while + retaining `BASE_BASH_LIBS_` globals and `__base_bash_libs_` internals. Legacy generic names are removed; see `docs/v2-symbol-map.md` for migration. - Made timed foreground-TTY invocations fail closed with a safe diagnostic; callers must provide a pipe or explicit non-terminal stdin for the v2 hard diff --git a/README.md b/README.md index a835c06..d36caf7 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ See [`lib/bash/README.md`](lib/bash/README.md) for the package layout. The v2 API charter, effect/status contract, and complete public-surface audit are in [`docs/v2-api-contract.md`](docs/v2-api-contract.md). The symbol-level mapping and migration aid are in [`docs/v2-symbol-map.md`](docs/v2-symbol-map.md). +The machine-readable module/API contract is in +[`base_api_manifest.yaml`](base_api_manifest.yaml), with its schema documented +in [`docs/api-manifest-schema.md`](docs/api-manifest-schema.md) and its +generated reference in [`docs/api-reference.md`](docs/api-reference.md). ## Installation and Usage @@ -197,7 +201,9 @@ event, immutable consumption, and the post-GA support contract. Release preparation and downstream Homebrew/Base handoffs are documented in [`docs/release-process.md`](docs/release-process.md). The machine-readable -release contract lives in [`base_manifest.yaml`](base_manifest.yaml). +release contract lives in [`base_manifest.yaml`](base_manifest.yaml); the +machine-readable API and module contract lives in +[`base_api_manifest.yaml`](base_api_manifest.yaml). ## License diff --git a/STANDARDS.md b/STANDARDS.md index 8cdd4e3..f66f37e 100644 --- a/STANDARDS.md +++ b/STANDARDS.md @@ -35,8 +35,9 @@ or chained source fragments. The v2 public namespace is deliberately collision-resistant so a library can be sourced into an existing application without taking generic names: -- Public functions use `base_bash_libs__` (with the two stdlib - lifecycle exceptions documented in `lib/bash/README.md`). +- Public functions use `base__` (with the two stdlib lifecycle + functions `base_init` and `base_require_version` documented in + `lib/bash/README.md`). - Framework-owned globals, environment controls, metadata, and load guards use `BASE_BASH_LIBS_...`. - Internal functions use `__base_bash_libs__...__` and are not callable diff --git a/base_api_manifest.yaml b/base_api_manifest.yaml new file mode 100644 index 0000000..5679542 --- /dev/null +++ b/base_api_manifest.yaml @@ -0,0 +1,179 @@ +schema_version: 1 +manifest_kind: base-bash-libs-api +manifest_version: 2.0.0 +minimum_bash: "4.2" +generated_reference: docs/api-reference.md +migration_inventory: docs/v2-api-contract.md + +namespace: + public_function_prefix: base_ + global_prefix: BASE_BASH_LIBS_ + internal_function_prefix: __base_bash_libs_ + lifecycle_functions: base_init,base_require_version + +# These fields are intentionally flat and line-oriented. Consumers can inspect +# the manifest with ordinary shell tools without installing a YAML runtime. +environment: + - BASE_BASH_LIBS_VERSION|readonly-global|loaded package version + - BASE_BASH_LIBS_DRY_RUN|caller-control|dry-run policy requested by the caller + - BASE_BASH_LIBS_PRIMARY_LOG|caller-control|primary persistent log sink + - BASE_CLI_PRIMARY_LOG|caller-input|Base shared primary log sink + - BASE_BASH_LIBS_LOG_DEBUG|caller-control|enable debug diagnostics + - BASE_BASH_LIBS_LOG_UTC|caller-control|render log timestamps in UTC + - BASE_BASH_LIBS_GIT_PULL_MAX_ATTEMPTS|caller-control|maximum git pull attempts + - NO_COLOR|caller-input|disable color output when set + - TMPDIR|caller-input|temporary directory policy + - GH_TOKEN|caller-input|GitHub CLI authentication input + - TZ|caller-input|caller-selected timezone for diagnostics + +optional_commands: git,gh,timeout,gtimeout + +artifacts: + - base_api_manifest.yaml|source|canonical API and module manifest + - docs/api-reference.md|generated|scripts/api-manifest generate|checked-in + - docs/api-manifest-schema.md|documentation|API manifest schema and tooling + - docs/v2-api-contract.md|documentation|normative API behavior charter + - docs/v2-symbol-map.md|documentation|v2 migration and symbol map + - lib/bash/README.md|documentation|package layout and namespace contract + - lib/bash/tests/test_helper.sh|test-support|shared BATS helpers + +# Every module remains one sourceable file (or the standalone launcher). The +# module-level contract applies to each symbol listed in public_symbols; the +# module README carries the call-specific signature and examples. +modules: + - name: std + kind: sourceable-library + source: lib/bash/std/lib_std.sh + documentation: lib/bash/std/README.md + tests: lib/bash/std/tests/lib_std.bats + dependencies: none + optional_commands: awk,basename,cat,command,cp,date,grep,mktemp,mv,printf,rm,sed,tput + artifacts: lib/bash/std/lib_std.sh,lib/bash/std/README.md,lib/bash/std/tests/lib_std.bats + public_symbols: base_init,base_require_version,base_std_add_to_path,base_std_ask_yes_no,base_std_assert_arg_count,base_std_assert_associative_array,base_std_assert_command_exists,base_std_assert_dir_exists,base_std_assert_executable,base_std_assert_file_exists,base_std_assert_function_exists,base_std_assert_indexed_array,base_std_assert_integer,base_std_assert_integer_range,base_std_assert_not_null,base_std_assert_variable_name,base_std_check_bash_version,base_std_command_path,base_std_dedupe_path,base_std_dump_trace,base_std_exit_if_error,base_std_fatal_error,base_std_function_exists,base_std_get_my_source_dir,base_std_import,base_std_is_dry_run,base_std_is_interactive,base_std_log_debug,base_std_log_debug_enter,base_std_log_debug_file,base_std_log_debug_leave,base_std_log_error,base_std_log_fatal,base_std_log_info,base_std_log_info_enter,base_std_log_info_file,base_std_log_info_leave,base_std_log_is_enabled,base_std_log_verbose,base_std_log_verbose_enter,base_std_log_verbose_file,base_std_log_verbose_leave,base_std_log_warn,base_std_make_temp_dir,base_std_make_temp_file,base_std_print_bold,base_std_print_error,base_std_print_info,base_std_print_message,base_std_print_path,base_std_print_success,base_std_print_tty,base_std_print_warn,base_std_register_cleanup_hook,base_std_register_cleanup_path,base_std_run,base_std_safe_cd,base_std_safe_mkdir,base_std_safe_touch,base_std_safe_truncate,base_std_safe_unalias,base_std_set_log_category_level,base_std_set_log_level,base_std_unregister_cleanup_hook,base_std_unregister_cleanup_path,base_std_wait_for_enter + signature_source: lib/bash/std/README.md + inputs: documented per symbol in the module README and API charter + outputs: documented per symbol; named outputs are caller-owned + statuses: documented per symbol; recoverable failures return status + side_effects: documented per symbol; sourcing is passive + stability: stable + since: 2.0.0 + deprecated: false + - name: file + kind: sourceable-library + source: lib/bash/file/lib_file.sh + documentation: lib/bash/file/README.md + tests: lib/bash/file/tests/lib_file.bats + dependencies: std + optional_commands: awk,cmp,cp,mv,rm,stat + artifacts: lib/bash/file/lib_file.sh,lib/bash/file/README.md,lib/bash/file/tests/lib_file.bats + public_symbols: base_file_section_exists,base_file_section_needs_update,base_file_update_file_section + signature_source: lib/bash/file/README.md + inputs: documented per symbol in the module README and API charter + outputs: documented per symbol; named outputs are caller-owned + statuses: documented per symbol; recoverable failures return status + side_effects: documented per symbol; sourcing is passive + stability: stable + since: 2.0.0 + deprecated: false + - name: git + kind: sourceable-library + source: lib/bash/git/lib_git.sh + documentation: lib/bash/git/README.md + tests: lib/bash/git/tests/lib_git.bats + dependencies: std + optional_commands: git,awk,grep,mktemp,rm,sleep + artifacts: lib/bash/git/lib_git.sh,lib/bash/git/README.md,lib/bash/git/tests/lib_git.bats + public_symbols: base_git_branch_merged_to_ref,base_git_branch_upstream,base_git_check_script_up_to_date,base_git_detect_default_branch,base_git_get_current_branch,base_git_list_remote_branches,base_git_list_worktree_branches,base_git_update_repo,base_git_worktree_path_for_branch + signature_source: lib/bash/git/README.md + inputs: documented per symbol in the module README and API charter + outputs: documented per symbol; named outputs are caller-owned + statuses: documented per symbol; recoverable failures return status + side_effects: documented per symbol; sourcing is passive + stability: stable + since: 2.0.0 + deprecated: false + - name: gh + kind: sourceable-library + source: lib/bash/gh/lib_gh.sh + documentation: lib/bash/gh/README.md + tests: lib/bash/gh/tests/lib_gh.bats + dependencies: std + optional_commands: gh,awk,grep,mktemp,rm,sleep,timeout,gtimeout + artifacts: lib/bash/gh/lib_gh.sh,lib/bash/gh/README.md,lib/bash/gh/tests/lib_gh.bats + public_symbols: base_gh_api_with_retry,base_gh_auth_status_diagnostics,base_gh_infer_repo_from_origin,base_gh_repo_default_branch,base_gh_repo_from_remote_url,base_gh_report_command_failure,base_gh_require_cli,base_gh_run + signature_source: lib/bash/gh/README.md + inputs: documented per symbol in the module README and API charter + outputs: documented per symbol; named outputs are caller-owned + statuses: documented per symbol; recoverable failures return status + side_effects: documented per symbol; sourcing is passive + stability: stable + since: 2.0.0 + deprecated: false + - name: str + kind: sourceable-library + source: lib/bash/str/lib_str.sh + documentation: lib/bash/str/README.md + tests: lib/bash/str/tests/lib_str.bats + dependencies: std + optional_commands: none + artifacts: lib/bash/str/lib_str.sh,lib/bash/str/README.md,lib/bash/str/tests/lib_str.bats + public_symbols: base_str_contains,base_str_ends_with,base_str_join,base_str_lower,base_str_ltrim,base_str_rtrim,base_str_split,base_str_starts_with,base_str_trim,base_str_upper + signature_source: lib/bash/str/README.md + inputs: documented per symbol in the module README and API charter + outputs: documented per symbol; named outputs are caller-owned + statuses: documented per symbol; recoverable failures return status + side_effects: documented per symbol; sourcing is passive + stability: stable + since: 2.0.0 + deprecated: false + - name: arg + kind: sourceable-library + source: lib/bash/arg/lib_arg.sh + documentation: lib/bash/arg/README.md + tests: lib/bash/arg/tests/lib_arg.bats + dependencies: std + optional_commands: awk,grep + artifacts: lib/bash/arg/lib_arg.sh,lib/bash/arg/README.md,lib/bash/arg/tests/lib_arg.bats + public_symbols: base_arg_parse + signature_source: lib/bash/arg/README.md + inputs: documented per symbol in the module README and API charter + outputs: documented per symbol; named outputs are caller-owned + statuses: documented per symbol; recoverable failures return status + side_effects: documented per symbol; sourcing is passive + stability: stable + since: 2.0.0 + deprecated: false + - name: list + kind: sourceable-library + source: lib/bash/list/lib_list.sh + documentation: lib/bash/list/README.md + tests: lib/bash/list/tests/lib_list.bats + dependencies: std + optional_commands: none + artifacts: lib/bash/list/lib_list.sh,lib/bash/list/README.md,lib/bash/list/tests/lib_list.bats + public_symbols: base_list_append,base_list_contains,base_list_length,base_list_prepend,base_list_remove,base_list_unique + signature_source: lib/bash/list/README.md + inputs: documented per symbol in the module README and API charter + outputs: documented per symbol; named outputs are caller-owned + statuses: documented per symbol; recoverable failures return status + side_effects: documented per symbol; sourcing is passive + stability: stable + since: 2.0.0 + deprecated: false + - name: launcher + kind: executable-launcher + source: bin/base-bash + documentation: docs/v2-api-contract.md + tests: tests/launcher.bats + dependencies: std + optional_commands: bash,basename,dirname,realpath,readlink + artifacts: bin/base-bash,docs/v2-api-contract.md,tests/launcher.bats + public_symbols: base_launcher_die,base_launcher_ensure_supported_bash,base_launcher_import_base_bash_lib,base_launcher_lib_dir_is_usable,base_launcher_package_root,base_launcher_resolve_lib_dir,base_launcher_resolve_path,base_launcher_run_script,base_launcher_source_stdlib,base_launcher_usage + signature_source: docs/v2-api-contract.md + inputs: executable arguments and package paths documented by the launcher + outputs: process status and delegated script output + statuses: launcher boundary may terminate; helpers return documented status + side_effects: starts a child script and sources the stdlib at the boundary + stability: stable + since: 2.0.0 + deprecated: false diff --git a/docs/api-manifest-schema.md b/docs/api-manifest-schema.md new file mode 100644 index 0000000..bb0999d --- /dev/null +++ b/docs/api-manifest-schema.md @@ -0,0 +1,75 @@ +# API and module manifest + +`base_api_manifest.yaml` is the authoritative v2 API/module inventory. It is +deliberately separate from `base_manifest.yaml`, which is the Base release and +developer-tooling manifest. The API manifest is a small, line-oriented YAML +subset so that consumers can inspect it with Bash, `awk`, or another YAML +reader; no Python, Ruby, `yq`, or `jq` runtime is required to consume the +library. + +## Schema + +The current schema is `1` and the manifest identifies itself with +`manifest_kind: base-bash-libs-api`. The required top-level fields are: + +| Field | Meaning | +| --- | --- | +| `schema_version` | Manifest schema compatibility number. | +| `manifest_version` | API release line represented by the manifest (`2.0.0` during v2 development). | +| `minimum_bash` | Minimum supported Bash runtime (`4.2`). | +| `generated_reference` | Checked-in API reference generated from this manifest. | +| `migration_inventory` | Normative v1-to-v2 behavior and symbol migration record. | +| `namespace` | Public function, global, internal, and lifecycle naming contract. | +| `environment` | Caller inputs and framework-owned globals, with kind and purpose. | +| `optional_commands` | External commands that are optional runtime capabilities. | +| `artifacts` | Manifest-owned source, documentation, generated, and test-support files. | +| `modules` | Ordered module records described below. | + +Each module declares its single source boundary, documentation, BATS suite, +dependencies, optional commands, package artifacts, public symbols, and API +metadata: + +- `kind` is `sourceable-library` for a sourceable `.sh` library or + `executable-launcher` for `bin/base-bash`. +- `dependencies` names other manifest modules. `scripts/api-manifest check` + rejects missing modules and dependency cycles. +- `public_symbols` is the complete exported function set. The checker compares + it with declarations in the source file and rejects both undocumented and + duplicate symbols. +- `signature_source` points to the README or charter containing call-specific + signatures and examples. `inputs`, `outputs`, `statuses`, and `side_effects` + provide the module-level contract inherited by each listed symbol. +- `stability`, `since`, and `deprecated` are required release metadata. A + future deprecation must add a migration-inventory entry before changing the + symbol. + +The artifact list makes packaging membership reviewable. Every module must +package its source, documentation, and tests. Generated files are checked for +drift rather than silently rewritten by validation. + +## Commands + +The Bash-native consumer tool is [`scripts/api-manifest`](../scripts/api-manifest): + +```bash +scripts/api-manifest check +scripts/api-manifest generate +scripts/api-manifest symbols +scripts/api-manifest module-paths +scripts/api-manifest source-paths +scripts/api-manifest test-paths +scripts/api-manifest artifact-paths +``` + +`check` validates schema and metadata, module/file existence, duplicate symbols, +source/manifest drift, dependency cycles, unsafe paths, packaging membership, +and the generated API reference. The repository validation suite obtains its +module source, test, and artifact paths from these commands instead of keeping +another hardcoded module list. + +## Single-file boundary + +The manifest describes library boundaries; it does not create a loader graph. +Each public sourceable library remains one physical file as required by +[`STANDARDS.md`](../STANDARDS.md). Adding a concern means adding or extending a +documented module boundary, not splitting an existing library into fragments. diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..83763b7 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,269 @@ +# Base Bash API Reference + +> This file is generated from `base_api_manifest.yaml`; edit the manifest or the module READMEs instead. + +## Contract + +- Manifest schema: 1 +- API version: 2.0.0 +- Minimum Bash: 4.2 + +Every module remains a single sourceable file. Signatures, inputs, outputs, +statuses, and side effects are normative in the linked module README and +[API charter](v2-api-contract.md). + +- Public functions: `base_` +- Globals: `BASE_BASH_LIBS_` +- Internals: `__base_bash_libs_` + +## Modules + +### `std` + +- Kind: `sourceable-library` +- Source: [`lib/bash/std/lib_std.sh`](../lib/bash/std/lib_std.sh) +- Documentation: [`lib/bash/std/README.md`](../lib/bash/std/README.md) +- Tests: [`lib/bash/std/tests/lib_std.bats`](../lib/bash/std/tests/lib_std.bats) +- Dependencies: `none` +- Optional commands: `awk,basename,cat,command,cp,date,grep,mktemp,mv,printf,rm,sed,tput` +- Stability: `stable`; since `2.0.0`; deprecated: `false` +- Inputs: documented per symbol in the module README and API charter +- Outputs: documented per symbol; named outputs are caller-owned +- Statuses: documented per symbol; recoverable failures return status +- Side effects: documented per symbol; sourcing is passive + +#### Public symbols + +- `base_init` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_require_version` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_add_to_path` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_ask_yes_no` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_arg_count` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_associative_array` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_command_exists` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_dir_exists` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_executable` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_file_exists` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_function_exists` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_indexed_array` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_integer` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_integer_range` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_not_null` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_assert_variable_name` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_check_bash_version` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_command_path` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_dedupe_path` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_dump_trace` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_exit_if_error` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_fatal_error` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_function_exists` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_get_my_source_dir` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_import` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_is_dry_run` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_is_interactive` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_debug` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_debug_enter` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_debug_file` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_debug_leave` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_error` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_fatal` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_info` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_info_enter` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_info_file` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_info_leave` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_is_enabled` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_verbose` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_verbose_enter` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_verbose_file` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_verbose_leave` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_log_warn` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_make_temp_dir` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_make_temp_file` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_print_bold` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_print_error` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_print_info` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_print_message` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_print_path` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_print_success` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_print_tty` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_print_warn` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_register_cleanup_hook` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_register_cleanup_path` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_run` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_safe_cd` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_safe_mkdir` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_safe_touch` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_safe_truncate` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_safe_unalias` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_set_log_category_level` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_set_log_level` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_unregister_cleanup_hook` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_unregister_cleanup_path` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). +- `base_std_wait_for_enter` — signature: see [`lib/bash/std/README.md`](../lib/bash/std/README.md). + +### `file` + +- Kind: `sourceable-library` +- Source: [`lib/bash/file/lib_file.sh`](../lib/bash/file/lib_file.sh) +- Documentation: [`lib/bash/file/README.md`](../lib/bash/file/README.md) +- Tests: [`lib/bash/file/tests/lib_file.bats`](../lib/bash/file/tests/lib_file.bats) +- Dependencies: `std` +- Optional commands: `awk,cmp,cp,mv,rm,stat` +- Stability: `stable`; since `2.0.0`; deprecated: `false` +- Inputs: documented per symbol in the module README and API charter +- Outputs: documented per symbol; named outputs are caller-owned +- Statuses: documented per symbol; recoverable failures return status +- Side effects: documented per symbol; sourcing is passive + +#### Public symbols + +- `base_file_section_exists` — signature: see [`lib/bash/file/README.md`](../lib/bash/file/README.md). +- `base_file_section_needs_update` — signature: see [`lib/bash/file/README.md`](../lib/bash/file/README.md). +- `base_file_update_file_section` — signature: see [`lib/bash/file/README.md`](../lib/bash/file/README.md). + +### `git` + +- Kind: `sourceable-library` +- Source: [`lib/bash/git/lib_git.sh`](../lib/bash/git/lib_git.sh) +- Documentation: [`lib/bash/git/README.md`](../lib/bash/git/README.md) +- Tests: [`lib/bash/git/tests/lib_git.bats`](../lib/bash/git/tests/lib_git.bats) +- Dependencies: `std` +- Optional commands: `git,awk,grep,mktemp,rm,sleep` +- Stability: `stable`; since `2.0.0`; deprecated: `false` +- Inputs: documented per symbol in the module README and API charter +- Outputs: documented per symbol; named outputs are caller-owned +- Statuses: documented per symbol; recoverable failures return status +- Side effects: documented per symbol; sourcing is passive + +#### Public symbols + +- `base_git_branch_merged_to_ref` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). +- `base_git_branch_upstream` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). +- `base_git_check_script_up_to_date` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). +- `base_git_detect_default_branch` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). +- `base_git_get_current_branch` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). +- `base_git_list_remote_branches` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). +- `base_git_list_worktree_branches` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). +- `base_git_update_repo` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). +- `base_git_worktree_path_for_branch` — signature: see [`lib/bash/git/README.md`](../lib/bash/git/README.md). + +### `gh` + +- Kind: `sourceable-library` +- Source: [`lib/bash/gh/lib_gh.sh`](../lib/bash/gh/lib_gh.sh) +- Documentation: [`lib/bash/gh/README.md`](../lib/bash/gh/README.md) +- Tests: [`lib/bash/gh/tests/lib_gh.bats`](../lib/bash/gh/tests/lib_gh.bats) +- Dependencies: `std` +- Optional commands: `gh,awk,grep,mktemp,rm,sleep,timeout,gtimeout` +- Stability: `stable`; since `2.0.0`; deprecated: `false` +- Inputs: documented per symbol in the module README and API charter +- Outputs: documented per symbol; named outputs are caller-owned +- Statuses: documented per symbol; recoverable failures return status +- Side effects: documented per symbol; sourcing is passive + +#### Public symbols + +- `base_gh_api_with_retry` — signature: see [`lib/bash/gh/README.md`](../lib/bash/gh/README.md). +- `base_gh_auth_status_diagnostics` — signature: see [`lib/bash/gh/README.md`](../lib/bash/gh/README.md). +- `base_gh_infer_repo_from_origin` — signature: see [`lib/bash/gh/README.md`](../lib/bash/gh/README.md). +- `base_gh_repo_default_branch` — signature: see [`lib/bash/gh/README.md`](../lib/bash/gh/README.md). +- `base_gh_repo_from_remote_url` — signature: see [`lib/bash/gh/README.md`](../lib/bash/gh/README.md). +- `base_gh_report_command_failure` — signature: see [`lib/bash/gh/README.md`](../lib/bash/gh/README.md). +- `base_gh_require_cli` — signature: see [`lib/bash/gh/README.md`](../lib/bash/gh/README.md). +- `base_gh_run` — signature: see [`lib/bash/gh/README.md`](../lib/bash/gh/README.md). + +### `str` + +- Kind: `sourceable-library` +- Source: [`lib/bash/str/lib_str.sh`](../lib/bash/str/lib_str.sh) +- Documentation: [`lib/bash/str/README.md`](../lib/bash/str/README.md) +- Tests: [`lib/bash/str/tests/lib_str.bats`](../lib/bash/str/tests/lib_str.bats) +- Dependencies: `std` +- Optional commands: `none` +- Stability: `stable`; since `2.0.0`; deprecated: `false` +- Inputs: documented per symbol in the module README and API charter +- Outputs: documented per symbol; named outputs are caller-owned +- Statuses: documented per symbol; recoverable failures return status +- Side effects: documented per symbol; sourcing is passive + +#### Public symbols + +- `base_str_contains` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_ends_with` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_join` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_lower` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_ltrim` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_rtrim` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_split` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_starts_with` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_trim` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). +- `base_str_upper` — signature: see [`lib/bash/str/README.md`](../lib/bash/str/README.md). + +### `arg` + +- Kind: `sourceable-library` +- Source: [`lib/bash/arg/lib_arg.sh`](../lib/bash/arg/lib_arg.sh) +- Documentation: [`lib/bash/arg/README.md`](../lib/bash/arg/README.md) +- Tests: [`lib/bash/arg/tests/lib_arg.bats`](../lib/bash/arg/tests/lib_arg.bats) +- Dependencies: `std` +- Optional commands: `awk,grep` +- Stability: `stable`; since `2.0.0`; deprecated: `false` +- Inputs: documented per symbol in the module README and API charter +- Outputs: documented per symbol; named outputs are caller-owned +- Statuses: documented per symbol; recoverable failures return status +- Side effects: documented per symbol; sourcing is passive + +#### Public symbols + +- `base_arg_parse` — signature: see [`lib/bash/arg/README.md`](../lib/bash/arg/README.md). + +### `list` + +- Kind: `sourceable-library` +- Source: [`lib/bash/list/lib_list.sh`](../lib/bash/list/lib_list.sh) +- Documentation: [`lib/bash/list/README.md`](../lib/bash/list/README.md) +- Tests: [`lib/bash/list/tests/lib_list.bats`](../lib/bash/list/tests/lib_list.bats) +- Dependencies: `std` +- Optional commands: `none` +- Stability: `stable`; since `2.0.0`; deprecated: `false` +- Inputs: documented per symbol in the module README and API charter +- Outputs: documented per symbol; named outputs are caller-owned +- Statuses: documented per symbol; recoverable failures return status +- Side effects: documented per symbol; sourcing is passive + +#### Public symbols + +- `base_list_append` — signature: see [`lib/bash/list/README.md`](../lib/bash/list/README.md). +- `base_list_contains` — signature: see [`lib/bash/list/README.md`](../lib/bash/list/README.md). +- `base_list_length` — signature: see [`lib/bash/list/README.md`](../lib/bash/list/README.md). +- `base_list_prepend` — signature: see [`lib/bash/list/README.md`](../lib/bash/list/README.md). +- `base_list_remove` — signature: see [`lib/bash/list/README.md`](../lib/bash/list/README.md). +- `base_list_unique` — signature: see [`lib/bash/list/README.md`](../lib/bash/list/README.md). + +### `launcher` + +- Kind: `executable-launcher` +- Source: [`bin/base-bash`](../bin/base-bash) +- Documentation: [`docs/v2-api-contract.md`](../docs/v2-api-contract.md) +- Tests: [`tests/launcher.bats`](../tests/launcher.bats) +- Dependencies: `std` +- Optional commands: `bash,basename,dirname,realpath,readlink` +- Stability: `stable`; since `2.0.0`; deprecated: `false` +- Inputs: executable arguments and package paths documented by the launcher +- Outputs: process status and delegated script output +- Statuses: launcher boundary may terminate; helpers return documented status +- Side effects: starts a child script and sources the stdlib at the boundary + +#### Public symbols + +- `base_launcher_die` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_ensure_supported_bash` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_import_base_bash_lib` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_lib_dir_is_usable` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_package_root` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_resolve_lib_dir` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_resolve_path` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_run_script` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_source_stdlib` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). +- `base_launcher_usage` — signature: see [`docs/v2-api-contract.md`](../docs/v2-api-contract.md). + diff --git a/scripts/api-manifest b/scripts/api-manifest new file mode 100755 index 0000000..1b20cc9 --- /dev/null +++ b/scripts/api-manifest @@ -0,0 +1,409 @@ +#!/usr/bin/env bash + +# Validate and consume the line-oriented YAML subset used by +# base_api_manifest.yaml. Keeping this tool Bash-only means a consumer can +# inspect the API contract without installing Python, Ruby, yq, or jq. + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" || exit 1 +repo_root="$(cd -- "$script_dir/.." && pwd -P)" || exit 1 +default_manifest="$repo_root/base_api_manifest.yaml" + +usage() { + cat >&2 <<'EOF' +Usage: + scripts/api-manifest check [manifest] + scripts/api-manifest generate [manifest] [output] + scripts/api-manifest symbols [manifest] + scripts/api-manifest module-paths [manifest] + scripts/api-manifest source-paths [manifest] + scripts/api-manifest test-paths [manifest] + scripts/api-manifest artifact-paths [manifest] +EOF +} + +error() { + printf 'API manifest error: %s\n' "$*" >&2 +} + +manifest_value() { + local manifest="$1" + local key="$2" + awk -v wanted="$key" ' + $0 ~ "^[[:space:]]*" wanted ":" { + value=$0 + sub("^[^:]+:[[:space:]]*", "", value) + gsub(/^"|"$/, "", value) + print value + exit + } + ' "$manifest" +} + +manifest_rows() { + local manifest="$1" + awk ' + function strip(value) { + sub(/^[[:space:]]+/, "", value) + sub(/[[:space:]]+$/, "", value) + gsub(/^"|"$/, "", value) + return value + } + function field(value) { + sub(/^[^:]+:[[:space:]]*/, "", value) + return strip(value) + } + function emit() { + if (name != "") { + printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", \ + name, kind, source, documentation, tests, dependencies, \ + optional_commands, artifacts, public_symbols, signature_source, \ + inputs, outputs, statuses, side_effects, stability, since, deprecated + } + } + /^modules:[[:space:]]*$/ { in_modules=1; next } + in_modules && /^ - name:/ { + emit() + name=field($0); kind=""; source=""; documentation=""; tests="" + dependencies=""; optional_commands=""; artifacts="" + public_symbols=""; signature_source=""; inputs=""; outputs="" + statuses=""; side_effects=""; stability=""; since=""; deprecated="" + next + } + in_modules && /^ kind:/ { kind=field($0); next } + in_modules && /^ source:/ { source=field($0); next } + in_modules && /^ documentation:/ { documentation=field($0); next } + in_modules && /^ tests:/ { tests=field($0); next } + in_modules && /^ dependencies:/ { dependencies=field($0); next } + in_modules && /^ optional_commands:/ { optional_commands=field($0); next } + in_modules && /^ artifacts:/ { artifacts=field($0); next } + in_modules && /^ public_symbols:/ { public_symbols=field($0); next } + in_modules && /^ signature_source:/ { signature_source=field($0); next } + in_modules && /^ inputs:/ { inputs=field($0); next } + in_modules && /^ outputs:/ { outputs=field($0); next } + in_modules && /^ statuses:/ { statuses=field($0); next } + in_modules && /^ side_effects:/ { side_effects=field($0); next } + in_modules && /^ stability:/ { stability=field($0); next } + in_modules && /^ since:/ { since=field($0); next } + in_modules && /^ deprecated:/ { deprecated=field($0); next } + END { emit() } + ' "$manifest" +} + +manifest_section_rows() { + local manifest="$1" + local section="$2" + awk -v wanted="$section" ' + $0 == wanted ":" { active=1; next } + active && /^[^[:space:]]/ { active=0 } + active && /^ - / { + value=$0 + sub(/^ - /, "", value) + print value + } + ' "$manifest" +} + +split_csv() { + local value="$1" + local item + local -a items=() + IFS=',' read -r -a items <<< "$value" + for item in "${items[@]}"; do + [[ -n "$item" ]] && printf '%s\n' "$item" + done +} + +source_symbols() { + local source="$1" + awk '/^base_[A-Za-z0-9_]+\(\)/ { symbol=$0; sub(/\(.*/, "", symbol); print symbol }' "$source" +} + +path_is_safe() { + local path="$1" + [[ -n "$path" && "$path" != /* && "$path" != *'..'* && "$path" != *$'\n'* && "$path" != *$'\r'* ]] +} + +module_cycle_check() { + local current="$1" + local dependency + if [[ "${module_visit[$current]-}" == visiting ]]; then + error "dependency cycle includes module '$current'" + return 1 + fi + [[ "${module_visit[$current]-}" == visited ]] && return 0 + module_visit[$current]=visiting + while IFS= read -r dependency; do + [[ -z "$dependency" || "$dependency" == none ]] && continue + [[ -n "${module_names[$dependency]-}" ]] || { + error "module '$current' depends on missing module '$dependency'" + return 1 + } + module_cycle_check "$dependency" || return 1 + done < <(split_csv "${module_dependencies[$current]}") + module_visit[$current]=visited + return 0 +} + +generate_reference() { + local manifest="$1" + local record field + local migration_inventory + local -a fields=() + local -a symbols=() + migration_inventory="$(manifest_value "$manifest" migration_inventory)" + printf '%s\n' '# Base Bash API Reference' '' \ + '> This file is generated from `base_api_manifest.yaml`; edit the manifest or the module READMEs instead.' '' \ + '## Contract' '' \ + "- Manifest schema: $(manifest_value "$manifest" schema_version)" \ + "- API version: $(manifest_value "$manifest" manifest_version)" \ + "- Minimum Bash: $(manifest_value "$manifest" minimum_bash)" '' \ + 'Every module remains a single sourceable file. Signatures, inputs, outputs,' \ + 'statuses, and side effects are normative in the linked module README and' \ + "[API charter](${migration_inventory#docs/})." '' + printf -- '- Public functions: `%s`\n' "$(manifest_value "$manifest" public_function_prefix)" + printf -- '- Globals: `%s`\n' "$(manifest_value "$manifest" global_prefix)" + printf -- '- Internals: `%s`\n\n' "$(manifest_value "$manifest" internal_function_prefix)" + printf '%s\n\n' '## Modules' + + while IFS=$'\t' read -r -a fields; do + [[ -n "${fields[0]-}" ]] || continue + printf '### `%s`\n\n' "${fields[0]}" + printf -- '- Kind: `%s`\n' "${fields[1]}" + printf -- '- Source: [`%s`](../%s)\n' "${fields[2]}" "${fields[2]}" + printf -- '- Documentation: [`%s`](../%s)\n' "${fields[3]}" "${fields[3]}" + printf -- '- Tests: [`%s`](../%s)\n' "${fields[4]}" "${fields[4]}" + printf -- '- Dependencies: `%s`\n' "${fields[5]}" + printf -- '- Optional commands: `%s`\n' "${fields[6]}" + printf -- '- Stability: `%s`; since `%s`; deprecated: `%s`\n' \ + "${fields[14]}" "${fields[15]}" "${fields[16]}" + printf -- '- Inputs: %s\n' "${fields[10]}" + printf -- '- Outputs: %s\n' "${fields[11]}" + printf -- '- Statuses: %s\n' "${fields[12]}" + printf -- '- Side effects: %s\n' "${fields[13]}" + printf '\n#### Public symbols\n\n' + IFS=',' read -r -a symbols <<< "${fields[8]}" + for field in "${symbols[@]}"; do + printf -- '- `%s` — signature: see [`%s`](../%s).\n' \ + "$field" "${fields[9]}" "${fields[9]}" + done + printf '\n' + done < <(manifest_rows "$manifest") +} + +check_manifest() { + local manifest="$1" + local value record field source_symbol symbol module artifact row + local module_count=0 + local -a fields=() + local -a symbols=() + local -a artifacts=() + local -a source_symbols_list=() + local -A manifest_symbols=() + local -A module_names=() + local -A module_dependencies=() + local -A module_visit=() + local -A module_sources=() + local -A module_tests=() + local -A module_docs=() + + [[ -f "$manifest" ]] || { error "manifest not found: $manifest"; return 1; } + for field in schema_version manifest_kind manifest_version minimum_bash generated_reference migration_inventory; do + value="$(manifest_value "$manifest" "$field")" + [[ -n "$value" ]] || { error "missing top-level field '$field'"; return 1; } + done + [[ "$(manifest_value "$manifest" schema_version)" == 1 ]] || { + error "schema_version must be 1" + return 1 + } + [[ "$(manifest_value "$manifest" manifest_kind)" == base-bash-libs-api ]] || { + error "manifest_kind must be base-bash-libs-api" + return 1 + } + [[ "$(manifest_value "$manifest" manifest_version)" =~ ^2[.]0[.]0([-.].*)?$ ]] || { + error "manifest_version must be on the v2.0.0 release line" + return 1 + } + [[ "$(manifest_value "$manifest" minimum_bash)" =~ ^4[.]2$ ]] || { + error "minimum_bash must be 4.2" + return 1 + } + [[ "$(manifest_value "$manifest" public_function_prefix)" == base_ ]] || { + error "public_function_prefix must be base_" + return 1 + } + [[ "$(manifest_value "$manifest" global_prefix)" == BASE_BASH_LIBS_ ]] || { + error "global_prefix must be BASE_BASH_LIBS_" + return 1 + } + [[ "$(manifest_value "$manifest" internal_function_prefix)" == __base_bash_libs_ ]] || { + error "internal_function_prefix must be __base_bash_libs_" + return 1 + } + + value="$(manifest_value "$manifest" generated_reference)" + path_is_safe "$value" || { error "generated_reference is not a safe relative path"; return 1; } + [[ -f "$repo_root/$value" ]] || { error "generated reference is missing: $value"; return 1; } + value="$(manifest_value "$manifest" migration_inventory)" + path_is_safe "$value" || { error "migration_inventory is not a safe relative path"; return 1; } + [[ -f "$repo_root/$value" ]] || { error "migration inventory is missing: $value"; return 1; } + + while IFS= read -r row; do + [[ "$row" == *'|'*'|'* ]] || { error "environment entry must be NAME|kind|description: $row"; return 1; } + done < <(manifest_section_rows "$manifest" environment) + value="$(manifest_value "$manifest" optional_commands)" + [[ -n "$value" ]] || { error "optional_commands must not be empty"; return 1; } + while IFS= read -r field; do + [[ "$field" =~ ^[a-zA-Z0-9._+-]+$ ]] || { error "invalid optional command '$field'"; return 1; } + done < <(split_csv "$value") + + while IFS=$'\t' read -r -a fields; do + [[ -n "${fields[0]-}" ]] || continue + module_count=$((module_count + 1)) + [[ -z "${module_names[${fields[0]}]-}" ]] || { error "duplicate module '${fields[0]}'"; return 1; } + module_names["${fields[0]}"]=1 + module_dependencies["${fields[0]}"]="${fields[5]}" + module_sources["${fields[0]}"]="${fields[2]}" + module_tests["${fields[0]}"]="${fields[4]}" + module_docs["${fields[0]}"]="${fields[3]}" + [[ "${fields[1]}" == sourceable-library || "${fields[1]}" == executable-launcher ]] || { + error "module '${fields[0]}' has an unsupported kind" + return 1 + } + for field in "${fields[2]}" "${fields[3]}" "${fields[4]}"; do + path_is_safe "$field" || { error "module '${fields[0]}' has an unsafe path '$field'"; return 1; } + [[ -f "$repo_root/$field" ]] || { error "module '${fields[0]}' path is missing: $field"; return 1; } + done + [[ "${fields[15]}" =~ ^2[.]0[.]0([-.].*)?$ ]] || { + error "module '${fields[0]}' has invalid since metadata" + return 1 + } + [[ "${fields[16]}" == true || "${fields[16]}" == false ]] || { + error "module '${fields[0]}' deprecated metadata must be true or false" + return 1 + } + IFS=',' read -r -a artifacts <<< "${fields[7]}" + for artifact in "${artifacts[@]}"; do + path_is_safe "$artifact" || { error "module '${fields[0]}' has an unsafe artifact '$artifact'"; return 1; } + [[ -f "$repo_root/$artifact" ]] || { error "module '${fields[0]}' artifact is missing: $artifact"; return 1; } + done + [[ ",${fields[7]}," == *",${fields[2]},"* ]] || { error "module '${fields[0]}' does not package its source"; return 1; } + [[ ",${fields[7]}," == *",${fields[3]},"* ]] || { error "module '${fields[0]}' does not package its documentation"; return 1; } + [[ ",${fields[7]}," == *",${fields[4]},"* ]] || { error "module '${fields[0]}' does not package its tests"; return 1; } + IFS=',' read -r -a symbols <<< "${fields[8]}" + [[ ${#symbols[@]} -gt 0 ]] || { error "module '${fields[0]}' declares no public symbols"; return 1; } + source_symbols_list=() + while IFS= read -r source_symbol; do source_symbols_list+=("$source_symbol"); done < <(source_symbols "$repo_root/${fields[2]}") + for symbol in "${symbols[@]}"; do + [[ "$symbol" =~ ^base_[a-zA-Z0-9_]+$ ]] || { error "invalid public symbol '$symbol'"; return 1; } + [[ -z "${manifest_symbols[$symbol]-}" ]] || { error "duplicate public symbol '$symbol'"; return 1; } + manifest_symbols[$symbol]=${fields[0]} + printf '%s\n' "${source_symbols_list[@]}" | grep -Fx "$symbol" >/dev/null || { + error "manifest symbol '$symbol' is not declared in ${fields[2]}" + return 1 + } + done + for source_symbol in "${source_symbols_list[@]}"; do + printf '%s\n' "${symbols[@]}" | grep -Fx "$source_symbol" >/dev/null || { + error "public source symbol '$source_symbol' is missing from module '${fields[0]}'" + return 1 + } + done + done < <(manifest_rows "$manifest") + ((module_count > 0)) || { error "manifest declares no modules"; return 1; } + + for module in "${!module_names[@]}"; do + module_cycle_check "$module" || return 1 + done + while IFS= read -r row; do + [[ -n "$row" ]] || continue + field="${row%%|*}" + path_is_safe "$field" || { error "artifact entry has an unsafe path '$field'"; return 1; } + [[ -f "$repo_root/$field" ]] || { error "artifact entry is missing: $field"; return 1; } + done < <(manifest_section_rows "$manifest" artifacts) + + return 0 +} + +manifest_or_default() { + if [[ -n "${1-}" ]]; then printf '%s\n' "$1"; else printf '%s\n' "$default_manifest"; fi +} + +main() { + local command="${1-}" + local manifest="" + local output="" + local row field + case "$command" in + check|generate|symbols|module-paths|source-paths|test-paths|artifact-paths) + shift + manifest="$(manifest_or_default "${1-}")" + [[ $# -eq 0 || $# -eq 1 || "$command" == generate ]] || { usage; return 2; } + ;; + -h|--help|help|"") + usage + return 2 + ;; + *) + usage + return 2 + ;; + esac + + case "$command" in + check) + check_manifest "$manifest" || return $? + output="$(mktemp "${TMPDIR:-/tmp}/base-api-manifest.XXXXXX")" || return 1 + if ! generate_reference "$manifest" > "$output"; then + rm -f "$output" + return 1 + fi + if ! cmp -s "$output" "$repo_root/$(manifest_value "$manifest" generated_reference)"; then + error "generated API reference is stale; run 'scripts/api-manifest generate'" + rm -f "$output" + return 1 + fi + rm -f "$output" + printf 'API manifest is valid.\n' + ;; + generate) + output="${2-}" + if [[ -n "$output" ]]; then + generate_reference "$manifest" > "$repo_root/$output" + else + generate_reference "$manifest" + fi + ;; + symbols) + check_manifest "$manifest" || return $? + while IFS=$'\t' read -r -a fields; do + IFS=',' read -r -a symbols <<< "${fields[8]}" + printf '%s\n' "${symbols[@]}" + done < <(manifest_rows "$manifest") + ;; + module-paths) + check_manifest "$manifest" || return $? + manifest_rows "$manifest" | cut -f2,4,5 + ;; + source-paths) + check_manifest "$manifest" || return $? + manifest_rows "$manifest" | cut -f3 + ;; + test-paths) + check_manifest "$manifest" || return $? + manifest_rows "$manifest" | cut -f5 + ;; + artifact-paths) + check_manifest "$manifest" || return $? + while IFS= read -r row; do + field="${row%%|*}" + printf '%s\n' "$field" + done < <(manifest_section_rows "$manifest" artifacts) + while IFS=$'\t' read -r -a fields; do + IFS=',' read -r -a artifacts <<< "${fields[7]}" + printf '%s\n' "${artifacts[@]}" + done < <(manifest_rows "$manifest") + ;; + esac +} + +main "$@" diff --git a/tests/api-manifest.bats b/tests/api-manifest.bats new file mode 100644 index 0000000..e969f82 --- /dev/null +++ b/tests/api-manifest.bats @@ -0,0 +1,52 @@ +#!/usr/bin/env bats + +load ../lib/bash/tests/test_helper.sh + +setup() { + setup_test_tmpdir +} + +@test "canonical API manifest validates and exposes the complete module graph" { + run "$BASE_REPO_ROOT/scripts/api-manifest" check + [ "$status" -eq 0 ] + [[ "$output" == *"API manifest is valid."* ]] + + run "$BASE_REPO_ROOT/scripts/api-manifest" module-paths + [ "$status" -eq 0 ] + [[ "$output" == *$'sourceable-library\tlib/bash/std/README.md\tlib/bash/std/tests/lib_std.bats'* ]] + [[ "$output" == *$'executable-launcher\tdocs/v2-api-contract.md\ttests/launcher.bats'* ]] +} + +@test "manifest symbols match source declarations and generated reference" { + manifest_symbols="$TEST_TMPDIR/manifest-symbols" + source_symbols="$TEST_TMPDIR/source-symbols" + + "$BASE_REPO_ROOT/scripts/api-manifest" symbols | sort -u > "$manifest_symbols" + { + grep -h -E '^base_[A-Za-z0-9_]+\(\)' \ + "$BASE_REPO_ROOT"/lib/bash/*/lib_*.sh + grep -h -E '^base_[A-Za-z0-9_]+\(\)' "$BASE_REPO_ROOT/bin/base-bash" + } | sed -E 's/^([A-Za-z_][A-Za-z0-9_]*)\(.*/\1/' | sort -u > "$source_symbols" + + run diff -u "$source_symbols" "$manifest_symbols" + [ "$status" -eq 0 ] + grep -F '`base_std_run`' "$BASE_REPO_ROOT/docs/api-reference.md" + grep -F '`base_launcher_run_script`' "$BASE_REPO_ROOT/docs/api-reference.md" +} + +@test "manifest rejects an invalid namespace and a dependency cycle" { + invalid_namespace="$TEST_TMPDIR/invalid-namespace.yaml" + cycle_manifest="$TEST_TMPDIR/cycle.yaml" + + sed 's/public_function_prefix: base_/public_function_prefix: bl_/' \ + "$BASE_REPO_ROOT/base_api_manifest.yaml" > "$invalid_namespace" + run "$BASE_REPO_ROOT/scripts/api-manifest" check "$invalid_namespace" + [ "$status" -ne 0 ] + [[ "$output" == *"public_function_prefix must be base_"* ]] + + perl -0pe 's/dependencies: none/dependencies: file/' \ + "$BASE_REPO_ROOT/base_api_manifest.yaml" > "$cycle_manifest" + run "$BASE_REPO_ROOT/scripts/api-manifest" check "$cycle_manifest" + [ "$status" -ne 0 ] + [[ "$output" == *"dependency cycle"* ]] +} diff --git a/tests/validate.sh b/tests/validate.sh index d0ce2ce..6464320 100755 --- a/tests/validate.sh +++ b/tests/validate.sh @@ -12,13 +12,17 @@ required_files=( LICENSE NOTICE base_manifest.yaml + base_api_manifest.yaml docs/versioning-policy.md docs/v2-api-contract.md docs/v2-symbol-map.md + docs/api-reference.md + docs/api-manifest-schema.md .github/workflows/project-intake.yml .github/workflows/tests.yml bin/base-bash scripts/release + scripts/api-manifest scripts/migrate-v2-symbols tests/fixtures/basectl-release-stub tests/bash-42-release-smoke.sh @@ -27,29 +31,9 @@ required_files=( examples/std-usage.sh examples/cookbook-cleanup-temp.sh examples/cookbook-args-lists-strings.sh - lib/bash/README.md - lib/bash/std/lib_std.sh - lib/bash/std/tests/lib_std.bats - lib/bash/file/lib_file.sh - lib/bash/file/tests/lib_file.bats - lib/bash/git/lib_git.sh - lib/bash/git/tests/lib_git.bats - lib/bash/gh/README.md - lib/bash/gh/lib_gh.sh - lib/bash/gh/tests/lib_gh.bats - lib/bash/str/README.md - lib/bash/str/lib_str.sh - lib/bash/str/tests/lib_str.bats - lib/bash/arg/README.md - lib/bash/arg/lib_arg.sh - lib/bash/arg/tests/lib_arg.bats - lib/bash/list/README.md - lib/bash/list/lib_list.sh - lib/bash/list/tests/lib_list.bats - lib/bash/tests/test_helper.sh - tests/launcher.bats tests/release.bats tests/namespace-contract.bats + tests/api-manifest.bats tests/lint-warnings.sh ) @@ -110,27 +94,24 @@ for file in "${required_files[@]}"; do } done -readme_test_paths=( - "lib/bash/std/README.md|lib/bash/std/tests/lib_std.bats" - "lib/bash/file/README.md|lib/bash/file/tests/lib_file.bats" - "lib/bash/git/README.md|lib/bash/git/tests/lib_git.bats" - "lib/bash/gh/README.md|lib/bash/gh/tests/lib_gh.bats" - "lib/bash/str/README.md|lib/bash/str/tests/lib_str.bats" - "lib/bash/arg/README.md|lib/bash/arg/tests/lib_arg.bats" - "lib/bash/list/README.md|lib/bash/list/tests/lib_list.bats" -) +manifest_artifacts="$(scripts/api-manifest artifact-paths)" || exit $? +while IFS= read -r file; do + [[ -n "$file" ]] && required_files+=("$file") +done <<<"$manifest_artifacts" -for mapping in "${readme_test_paths[@]}"; do - IFS='|' read -r readme test_path <<<"$mapping" +manifest_module_paths="$(scripts/api-manifest module-paths)" || exit $? +while IFS=$'\t' read -r module_kind readme test_path; do + [[ -n "$module_kind" && -n "$readme" && -n "$test_path" ]] || continue [[ -f "$test_path" ]] || { printf 'Documented BATS path does not exist: %s\n' "$test_path" >&2 exit 1 } + [[ "$module_kind" != sourceable-library ]] && continue grep -F "$test_path" "$readme" >/dev/null || { printf 'README does not document its BATS path: %s -> %s\n' "$readme" "$test_path" >&2 exit 1 } -done +done <<<"$manifest_module_paths" printf 'Repository baseline is present.\n' @@ -206,8 +187,15 @@ for command in shellcheck bats; do } done +manifest_source_paths="$(scripts/api-manifest source-paths)" || exit $? +manifest_shellcheck_paths=() +while IFS= read -r file; do + [[ -n "$file" ]] && manifest_shellcheck_paths+=("$file") +done <<<"$manifest_source_paths" + run_stage "ShellCheck error profile" shellcheck --severity=error \ bin/base-bash \ + scripts/api-manifest \ scripts/release \ scripts/migrate-v2-symbols \ tests/fixtures/basectl-release-stub \ @@ -219,30 +207,21 @@ run_stage "ShellCheck error profile" shellcheck --severity=error \ examples/std-usage.sh \ examples/cookbook-cleanup-temp.sh \ examples/cookbook-args-lists-strings.sh \ - lib/bash/std/lib_std.sh \ - lib/bash/file/lib_file.sh \ - lib/bash/git/lib_git.sh \ - lib/bash/gh/lib_gh.sh \ - lib/bash/str/lib_str.sh \ - lib/bash/arg/lib_arg.sh \ - lib/bash/list/lib_list.sh \ lib/bash/tests/test_helper.sh \ - tests/launcher.bats \ + "${manifest_shellcheck_paths[@]}" \ tests/release.bats \ - tests/namespace-contract.bats + tests/namespace-contract.bats \ + tests/api-manifest.bats bats_files=( - tests/launcher.bats tests/release.bats tests/namespace-contract.bats - lib/bash/std/tests/lib_std.bats - lib/bash/file/tests/lib_file.bats - lib/bash/git/tests/lib_git.bats - lib/bash/gh/tests/lib_gh.bats - lib/bash/str/tests/lib_str.bats - lib/bash/arg/tests/lib_arg.bats - lib/bash/list/tests/lib_list.bats + tests/api-manifest.bats ) +manifest_test_paths="$(scripts/api-manifest test-paths)" || exit $? +while IFS= read -r file; do + [[ -n "$file" ]] && bats_files+=("$file") +done <<<"$manifest_test_paths" run_stage "BATS test suites" bats \ "${bats_files[@]}" || exit $?