diff --git a/README.md b/README.md index 84fd5a7..d2aeea8 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Training Case Engine is the external-facing product training module. The public - Training API validation failures are deterministic public validation errors: `400` with `invalid_json_body`, `400` with `missing_required_json_field:`, `400` with `invalid_json_field:`, `400` with `invalid_path_param:`, or `400` with `invalid_limit_query_param:limit`. - Missing Training cases return `404` with `not_found`. - Unexpected Training API failures return `500` with `internal_server_error`; the public response masks raw internals, and responses do not echo raw internals. -- Package CLI public errors for invalid input paths and invalid output targets emit one-line JSON with `passed`, `input`, `output`, and `error`; stable path codes include `input_file_not_found` and `input_path_is_directory`. +- Package CLI public errors for invalid input paths and invalid output targets emit one-line JSON with `passed`, `input`, `output`, and `error`; stable public error codes include `input_file_not_found`, `input_path_is_directory`, `output_path_is_directory`, and `output_path_unwritable`. ## Local Docker deployment @@ -56,7 +56,7 @@ test "$(docker ps --filter name=^/ai-ict$ --format '{{.Names}}' | wc -l | tr -d test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy" ``` -- Offline local demo readiness check, which does not invoke Docker: +- Check the documented local demo readiness without starting containers; this offline check does not invoke Docker: ```bash .venv/bin/python scripts/check_local_demo_readiness.py @@ -83,6 +83,7 @@ test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy" - `python3 scripts/run_autonomous_cycle.py` - run the unattended cycle: offline checks, database probe, readiness checks, then guarded pipeline when the database is available; prints `Next actions` for the next unattended step - `python3 scripts/run_database_readiness.py` - run read-only database readiness checks for connection, schema, seed data, and candle availability; prints `next_actions` and `action_plan` in JSON mode - `.venv/bin/python scripts/run_offline_checks.py` - run the broader offline check suite, including the M35 acceptance posture through the Training Case acceptance manifest for focused milestone test suites, the Training Case package smoke registered as `training_case_package`, dashboard syntax validation as `dashboard_js_syntax`, and dashboard and public docs privacy smoke coverage; this public validation runs as offline checks without external services +- `.venv/bin/python scripts/run_training_case_package.py --help` - show the offline package smoke help for public package import/export behavior, public export/public output semantics, and stable public error codes for invalid input paths and invalid output targets - `.venv/bin/python scripts/run_training_case_package.py --input tests/fixtures/training_case_package_m6.json --output /tmp/training_case_public_export.json` - run the Training Case offline package smoke, an import/export smoke with local files, and write public output as the public export in the public package shape; exported public JSON omits internal solution details - `python3 scripts/run_migration_static_checks.py` - check migration order and required core schema without connecting to the database - `python3 scripts/run_script_static_checks.py` - check automation scripts for unsafe ambient environment fallback patterns diff --git a/docs/examples/training_case_package_smoke_m6.md b/docs/examples/training_case_package_smoke_m6.md index 7dac6d0..ac19d10 100644 --- a/docs/examples/training_case_package_smoke_m6.md +++ b/docs/examples/training_case_package_smoke_m6.md @@ -10,6 +10,12 @@ Run the offline package smoke as a local import/export smoke: The command uses local files only. The written export is public output in the public package shape and omits internal solution details. +Discover the same public package import/export smoke and public error contract from CLI help: + +```bash +.venv/bin/python scripts/run_training_case_package.py --help +``` + The broader offline check suite includes this same Training Case smoke as `training_case_package`: ```bash @@ -17,3 +23,5 @@ The broader offline check suite includes this same Training Case smoke as `train ``` On success, stdout is one JSON object with `passed`, `input`, `output`, `imported`, and `exported`. `--output` writes only the public export JSON shown by `output`; it does not include internal solution details. + +On public errors for invalid input paths or invalid output targets, stdout is one-line JSON with `passed`, `input`, `output`, and `error`. Stable public error codes include `input_file_not_found`, `input_path_is_directory`, `output_path_is_directory`, and `output_path_unwritable`; the message masks raw internals. diff --git a/docs/training_case_engine_m1_m6_product_readme.md b/docs/training_case_engine_m1_m6_product_readme.md index faefb9d..e81682e 100644 --- a/docs/training_case_engine_m1_m6_product_readme.md +++ b/docs/training_case_engine_m1_m6_product_readme.md @@ -45,7 +45,7 @@ test "$(docker ps --filter name=^/ai-ict$ --format '{{.Names}}' | wc -l | tr -d test "$(docker inspect -f '{{.State.Health.Status}}' ai-ict)" = "healthy" ``` -Offline local demo readiness check, which does not invoke Docker: +- Check the documented local demo readiness without starting containers; this offline check does not invoke Docker: ```bash .venv/bin/python scripts/check_local_demo_readiness.py @@ -78,6 +78,12 @@ Use the offline package smoke to check import plus public export and public outp --output /tmp/training_case_public_export.json ``` +Discover the same public package import/export behavior and public error contract from CLI help: + +```bash +.venv/bin/python scripts/run_training_case_package.py --help +``` + The broader offline check suite also includes this package smoke as `training_case_package` and dashboard JavaScript syntax validation as `dashboard_js_syntax`: ```bash @@ -99,7 +105,7 @@ Training public error catalog: - Invalid list limits are deterministic public validation errors and return `400` with `invalid_limit_query_param:limit`; valid limits are `1` through `100`. - Missing cases return `404` with `not_found`. - Unexpected failures return `500` with `internal_server_error`; the public response masks raw internals, and responses do not echo raw internals. -- Package CLI public errors for invalid input paths and invalid output targets emit one-line JSON with `passed`, `input`, `output`, and `error`; stable path codes include `input_file_not_found` and `input_path_is_directory`. +- Package CLI public errors for invalid input paths and invalid output targets emit one-line JSON with `passed`, `input`, `output`, and `error`; stable public error codes include `input_file_not_found`, `input_path_is_directory`, `output_path_is_directory`, and `output_path_unwritable`. Package import rejects private internal solution fields. Package export is stable public output JSON and omits private solution material. diff --git a/scripts/run_training_case_package.py b/scripts/run_training_case_package.py index 43f8eb6..5c907a6 100644 --- a/scripts/run_training_case_package.py +++ b/scripts/run_training_case_package.py @@ -94,9 +94,20 @@ def main(argv=None) -> int: def _build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description="Smoke-check a Training Case package with local files.") - parser.add_argument("--input", default=str(DEFAULT_FIXTURE), help="Training Case package JSON path.") - parser.add_argument("--output", help="Optional public export JSON path.") + parser = argparse.ArgumentParser( + prog="scripts/run_training_case_package.py", + description="Offline package smoke: Training Case import/export smoke with local files.", + epilog=( + "Imports a public package from --input and writes public export/public output " + "to --output when set. On public errors, stdout is one-line JSON with " + "passed, input, output, and error. Stable public error codes include " + "input_file_not_found, input_path_is_directory, output_path_is_directory, " + "and output_path_unwritable." + ), + ) + input_help = f"Public package JSON path to import (default: {DEFAULT_FIXTURE.relative_to(ROOT)})." + parser.add_argument("--input", default=str(DEFAULT_FIXTURE), help=input_help) + parser.add_argument("--output", help="Optional public export JSON path; writes public output only.") return parser diff --git a/tests/unit/test_training_case_docs_cli_m10.py b/tests/unit/test_training_case_docs_cli_m10.py index 7668dc5..02e9217 100644 --- a/tests/unit/test_training_case_docs_cli_m10.py +++ b/tests/unit/test_training_case_docs_cli_m10.py @@ -27,6 +27,7 @@ OFFLINE_CHECK_NAME = "training_case_package" OFFLINE_CHECK_DOC_NAME = f"`{OFFLINE_CHECK_NAME}`" PACKAGE_SMOKE_SCRIPT = "scripts/run_training_case_package.py" PACKAGE_SMOKE_COMMAND = f".venv/bin/python {PACKAGE_SMOKE_SCRIPT}" +PACKAGE_SMOKE_HELP_COMMAND = f"{PACKAGE_SMOKE_COMMAND} --help" DASHBOARD_SYNTAX_CHECK_NAME = "dashboard_js_syntax" DASHBOARD_SYNTAX_CHECK_PATH = "src/web/app.js" OFFLINE_CHECK_REGISTRY_NAMES = ( @@ -63,6 +64,31 @@ PACKAGE_IMPORT_EXPORT_SURFACE_TEXT = ( "public export", "public output", ) +PACKAGE_CLI_HELP_DISCOVERY_TEXT = ( + "offline package smoke", + "import/export smoke", + "public package", + "public export", + "public output", + "one-line json", + "stable public error codes", + "input_file_not_found", + "input_path_is_directory", + "output_path_is_directory", + "output_path_unwritable", +) +PACKAGE_DOC_HELP_ERROR_TEXT = ( + PACKAGE_SMOKE_HELP_COMMAND, + "one-line JSON", + "`passed`", + "`input`", + "`output`", + "`error`", + "`input_file_not_found`", + "`input_path_is_directory`", + "`output_path_is_directory`", + "`output_path_unwritable`", +) PRODUCT_PACKAGE_WORDING = ( CURRENT_TRAINING_DOC_MATURITY_MARKER, "offline checks registry smoke", @@ -97,6 +123,8 @@ TRAINING_M24_M29_PACKAGE_DOCS_TEST_INVENTORY = { "test_readme_and_product_docs_advertise_public_offline_smoke_contract", "test_product_readme_uses_current_maturity_and_public_private_package_language", "test_public_package_smoke_docs_lock_import_export_privacy_language", + "test_cli_help_discloses_offline_public_import_export_and_error_contract", + "test_package_smoke_docs_publish_help_and_stable_public_error_wording", "test_m1_m28_package_smoke_docs_cover_static_import_export_surface", ), "offline_registry_coverage": ( @@ -121,6 +149,8 @@ TRAINING_M33_PACKAGE_FIXTURE_IMPORT_EXPORT_PRIVACY_INVENTORY = { "package_smoke_docs_command_help": ( "test_example_command_uses_supported_cli_flags_and_fixture", "test_public_package_smoke_docs_lock_import_export_privacy_language", + "test_cli_help_discloses_offline_public_import_export_and_error_contract", + "test_package_smoke_docs_publish_help_and_stable_public_error_wording", "test_m1_m28_package_smoke_docs_cover_static_import_export_surface", ), "fixture_public_shape": ( @@ -506,9 +536,9 @@ class TrainingCaseDocsCliM10Tests(unittest.TestCase): unique_inventory_names = set(inventory_names) product_text = PRODUCT_README.read_text(encoding="utf-8") - self.assertEqual(len(test_names), 15) - self.assertEqual(len(inventory_names), 16) - self.assertEqual(len(unique_inventory_names), 14) + self.assertEqual(len(test_names), 17) + self.assertEqual(len(inventory_names), 18) + self.assertEqual(len(unique_inventory_names), 16) self.assertEqual(sorted(TRAINING_M24_M29_PACKAGE_DOCS_TEST_INVENTORY), [ "m1_m28_maturity_wording", "offline_registry_coverage", @@ -548,8 +578,8 @@ class TrainingCaseDocsCliM10Tests(unittest.TestCase): sorted(TRAINING_M33_PACKAGE_FIXTURE_IMPORT_EXPORT_PRIVACY_INVENTORY), sorted(TRAINING_M33_REQUIRED_INVENTORY_GROUPS), ) - self.assertEqual(len(m33_inventory_names), 17) - self.assertEqual(len(m33_unique_inventory_names), 14) + self.assertEqual(len(m33_inventory_names), 19) + self.assertEqual(len(m33_unique_inventory_names), 16) self.assertIn( "test_training_readmes_name_dashboard_privacy_and_offline_registry_coverage", documentation_consistency_test_names, @@ -580,6 +610,42 @@ class TrainingCaseDocsCliM10Tests(unittest.TestCase): self.assertNotIn(phrase, lower_text) self.assertNotIn(PUBLIC_FIELD, text) + def test_cli_help_discloses_offline_public_import_export_and_error_contract(self) -> None: + parser = training_cli._build_parser() + help_text = parser.format_help() + lower_help = help_text.lower() + stdout = io.StringIO() + stderr = io.StringIO() + + with ( + contextlib.redirect_stdout(stdout), + contextlib.redirect_stderr(stderr), + self.assertRaises(SystemExit) as raised, + ): + training_cli.main(["--help"]) + + self.assertEqual(raised.exception.code, 0) + self.assertEqual(stderr.getvalue(), "") + self.assertEqual(stdout.getvalue(), help_text) + self.assertIn(PACKAGE_SMOKE_SCRIPT, help_text) + self.assertIn(FIXTURE_REF, help_text) + self.assertNotIn(PUBLIC_FIELD, help_text) + for snippet in PACKAGE_CLI_HELP_DISCOVERY_TEXT: + with self.subTest(snippet=snippet): + self.assertIn(snippet, lower_help) + + def test_package_smoke_docs_publish_help_and_stable_public_error_wording(self) -> None: + for path in PACKAGE_SMOKE_SURFACE_DOCS: + text = path.read_text(encoding="utf-8") + lower_text = text.lower() + + with self.subTest(path=path.name): + self.assertIn("invalid output targets", lower_text) + self.assertNotIn(PUBLIC_FIELD, text) + for snippet in PACKAGE_DOC_HELP_ERROR_TEXT: + with self.subTest(snippet=snippet): + self.assertIn(snippet, text) + def test_m1_m28_package_smoke_docs_cover_static_import_export_surface(self) -> None: product_text = PRODUCT_README.read_text(encoding="utf-8") cli_flags = _package_smoke_flags_from_source()