feat(devkit): add resumable workspace automation and UI review tooling
Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
This commit is contained in:
+270
-76
@@ -1,13 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# The JSON catalog is the only phase order/metadata authority. Check commands
|
||||
# remain in the marked functions below; devkit invokes this script, not copies.
|
||||
META_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
ROOT="${GOVOPLAN_CORE_ROOT:-$META_ROOT/../govoplan-core}"
|
||||
|
||||
FOCUSED_MODE=run
|
||||
FOCUSED_PHASE=""
|
||||
FOCUSED_JSON=0
|
||||
|
||||
focused_usage() {
|
||||
echo "Usage: check-focused.sh [--phase ID] | --list-phases [--json]"
|
||||
echo "Without --phase, run every canonical phase in catalog order (fail fast)."
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--phase)
|
||||
if [[ "$FOCUSED_MODE" != run || -n "$FOCUSED_PHASE" || $# -lt 2 || -z "$2" || "$2" == --* ]]; then
|
||||
echo "check-focused: --phase requires one ID and cannot be combined with listing." >&2
|
||||
exit 2
|
||||
fi
|
||||
FOCUSED_PHASE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--list-phases)
|
||||
if [[ "$FOCUSED_MODE" != run || -n "$FOCUSED_PHASE" ]]; then
|
||||
echo "check-focused: duplicate or incompatible phase selection." >&2
|
||||
exit 2
|
||||
fi
|
||||
FOCUSED_MODE=list
|
||||
shift
|
||||
;;
|
||||
--json)
|
||||
if [[ "$FOCUSED_JSON" == 1 ]]; then
|
||||
echo "check-focused: duplicate --json." >&2
|
||||
exit 2
|
||||
fi
|
||||
FOCUSED_JSON=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
focused_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "check-focused: unknown argument: $1" >&2
|
||||
focused_usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ "$FOCUSED_JSON" == 1 && "$FOCUSED_MODE" != list ]]; then
|
||||
echo "check-focused: --json is supported only with --list-phases." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Metadata-only operations need standard Python, not an installed product venv,
|
||||
# Node, npm, a Core checkout, or any temporary/state directory.
|
||||
FOCUSED_METADATA_PYTHON="$(command -v python3)" || {
|
||||
echo "check-focused: Python 3 is required to read phase metadata." >&2
|
||||
exit 127
|
||||
}
|
||||
FOCUSED_SELECTION="$(
|
||||
PYTHONDONTWRITEBYTECODE=1 "$FOCUSED_METADATA_PYTHON" -I -S - \
|
||||
"$META_ROOT/tools/checks/focused-phases.json" "$FOCUSED_MODE" "$FOCUSED_PHASE" "$FOCUSED_JSON" <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
|
||||
try:
|
||||
path = Path(sys.argv[1])
|
||||
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0))
|
||||
with os.fdopen(descriptor, "rb") as handle:
|
||||
status = os.fstat(handle.fileno())
|
||||
if not stat.S_ISREG(status.st_mode) or status.st_size > 1024 * 1024:
|
||||
raise ValueError("phase metadata must be a bounded regular file")
|
||||
encoded = handle.read(1024 * 1024 + 1)
|
||||
if len(encoded) > 1024 * 1024:
|
||||
raise ValueError("phase metadata exceeds its size bound")
|
||||
|
||||
def unique(pairs):
|
||||
result = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
raise ValueError("duplicate phase metadata key")
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
catalog = json.loads(encoded, object_pairs_hook=unique)
|
||||
if not isinstance(catalog, dict) or set(catalog) != {"schema_version", "phases"} or type(catalog["schema_version"]) is not int or catalog["schema_version"] != 1:
|
||||
raise ValueError("unsupported phase metadata schema")
|
||||
phases = catalog["phases"]
|
||||
if not isinstance(phases, list) or not 1 <= len(phases) <= 32:
|
||||
raise ValueError("phase catalog requires 1–32 definitions")
|
||||
seen = set()
|
||||
fields = {"id", "title", "cwd", "order_after", "depends_on", "resources", "outputs", "notes"}
|
||||
for phase in phases:
|
||||
if not isinstance(phase, dict) or set(phase) != fields:
|
||||
raise ValueError("invalid phase metadata fields")
|
||||
identity = phase["id"]
|
||||
if not isinstance(identity, str) or not re.fullmatch(r"[a-z][a-z0-9-]{0,63}", identity) or identity in seen:
|
||||
raise ValueError("invalid or duplicate phase ID")
|
||||
if not isinstance(phase["title"], str) or not phase["title"].strip() or len(phase["title"]) > 256 or any(ord(char) < 32 for char in phase["title"]):
|
||||
raise ValueError("invalid phase title")
|
||||
if phase["cwd"] not in {"core", "meta", "core-webui", "access-webui"}:
|
||||
raise ValueError("unsupported phase working directory")
|
||||
for field in ("order_after", "depends_on", "resources", "outputs", "notes"):
|
||||
values = phase[field]
|
||||
if not isinstance(values, list) or len(values) > 64 or any(not isinstance(value, str) or not value or len(value) > 4096 or "\0" in value for value in values):
|
||||
raise ValueError("invalid phase list: " + field)
|
||||
if len(set(values)) != len(values):
|
||||
raise ValueError("duplicate phase list value: " + field)
|
||||
if (set(phase["order_after"]) | set(phase["depends_on"])) - seen:
|
||||
raise ValueError("phase prerequisites must precede their consumer")
|
||||
seen.add(identity)
|
||||
mode, selected, as_json = sys.argv[2:]
|
||||
if selected and selected not in seen:
|
||||
raise ValueError("unknown phase: " + selected)
|
||||
if mode == "list":
|
||||
print(json.dumps(catalog, indent=2) if as_json == "1" else "\n".join(phase["id"] + "\t" + phase["title"] for phase in phases))
|
||||
else:
|
||||
print("\n".join(phase["id"] for phase in phases if not selected or phase["id"] == selected))
|
||||
except (OSError, ValueError, TypeError, RecursionError) as exc:
|
||||
print("check-focused: " + str(exc), file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
PY
|
||||
)"
|
||||
|
||||
if [[ "$FOCUSED_MODE" == list ]]; then
|
||||
printf '%s\n' "$FOCUSED_SELECTION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
focused_setup() {
|
||||
WORKSPACE_ROOT="${GOVOPLAN_WORKSPACE_ROOT:-$(dirname "$META_ROOT")}"
|
||||
export GOVOPLAN_WORKSPACE_ROOT="$WORKSPACE_ROOT"
|
||||
ROOT="${GOVOPLAN_CORE_ROOT:-$WORKSPACE_ROOT/govoplan-core}"
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
VENV_ROOT="${GOVOPLAN_VENV_ROOT:-$META_ROOT/.venv}"
|
||||
PYTHON="${PYTHON:-$VENV_ROOT/bin/python}"
|
||||
NODE="/home/zemion/.nvm/versions/node/v22.22.3/bin"
|
||||
NPM="$NODE/npm"
|
||||
NODE="$(command -v "${NODE:-node}")" || { echo "check-focused: Node is unavailable; run ./devkit doctor." >&2; exit 127; }
|
||||
NPM="$(command -v "${NPM:-npm}")" || { echo "check-focused: npm is unavailable; run ./devkit doctor." >&2; exit 127; }
|
||||
NODE_BIN="$(dirname "$NODE")"
|
||||
WEBUI_BIN="$ROOT/webui/node_modules/.bin"
|
||||
NPM_USERCONFIG="$(mktemp "${TMPDIR:-/tmp}/govoplan-npmrc.XXXXXXXX")"
|
||||
|
||||
@@ -19,7 +157,7 @@ trap 'rm -f "$NPM_USERCONFIG"' EXIT
|
||||
exit 127
|
||||
}
|
||||
|
||||
export PATH="$WEBUI_BIN:$NODE:$PATH"
|
||||
export PATH="$WEBUI_BIN:$NODE_BIN:$PATH"
|
||||
export NPM_CONFIG_USERCONFIG="$NPM_USERCONFIG"
|
||||
export GOVOPLAN_NPM_USERCONFIG="$NPM_USERCONFIG"
|
||||
unset npm_config_tmp NPM_CONFIG_TMP
|
||||
@@ -27,28 +165,41 @@ unset npm_config_tmp NPM_CONFIG_TMP
|
||||
# Validate the current sibling checkouts even when a newly added module has not
|
||||
# yet been installed into an existing development virtualenv.
|
||||
SOURCE_PYTHONPATH=""
|
||||
for source_dir in "$META_ROOT"/../govoplan*/src; do
|
||||
for source_dir in "$WORKSPACE_ROOT"/govoplan*/src; do
|
||||
[ -d "$source_dir" ] || continue
|
||||
SOURCE_PYTHONPATH="${SOURCE_PYTHONPATH:+$SOURCE_PYTHONPATH:}$source_dir"
|
||||
done
|
||||
export PYTHONPATH="${SOURCE_PYTHONPATH}${PYTHONPATH:+:$PYTHONPATH}"
|
||||
}
|
||||
|
||||
focused_phase_preflight() {
|
||||
# devkit-phase: preflight begin
|
||||
cd "$ROOT"
|
||||
|
||||
GOVOPLAN_CORE_ROOT="$ROOT" PYTHON="$PYTHON" CHECK_TESTCLIENT_DEPRECATIONS=1 bash "$META_ROOT/tools/checks/check-dependency-hygiene.sh"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-contracts.py" --no-impact
|
||||
PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-manifest-shapes.py" --require-architecture
|
||||
PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-dsar-coverage.py"
|
||||
"$NODE/node" "$META_ROOT/tests/test-jsx-value-imports.mjs"
|
||||
"$NODE/node" "$META_ROOT/tools/checks/check-jsx-value-imports.mjs"
|
||||
"$NODE/node" "$META_ROOT/../govoplan-files/webui/scripts/test-archive-client.mjs"
|
||||
"$NODE" "$META_ROOT/tests/test-jsx-value-imports.mjs"
|
||||
"$NODE" "$META_ROOT/tools/checks/check-jsx-value-imports.mjs"
|
||||
"$NODE" "$META_ROOT/tests/test-heading-help.mjs"
|
||||
"$NODE" "$META_ROOT/tools/checks/check-heading-help.mjs"
|
||||
"$NODE" --test "$META_ROOT/tests/test-devkit-display-labels.mjs"
|
||||
"$NODE" --test "$ROOT/webui/tests/component-test-runner.test.mjs"
|
||||
"$NODE" "$WORKSPACE_ROOT/govoplan-files/webui/scripts/test-archive-client.mjs"
|
||||
# devkit-phase: preflight end
|
||||
}
|
||||
|
||||
focused_phase_tooling() {
|
||||
# devkit-phase: tooling begin
|
||||
cd "$META_ROOT"
|
||||
"$PYTHON" tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
|
||||
"$PYTHON" tools/inventory/platform-interface-inventory.py --workspace-root "$WORKSPACE_ROOT" --strict-declarations --strict-endpoints
|
||||
"$PYTHON" tools/repo/sync-module-package-workflows.py --check
|
||||
"$PYTHON" tools/release/generate-developer-meta-package.py --check
|
||||
"$PYTHON" tools/checks/check-webui-package-facades.py
|
||||
"$PYTHON" -m unittest tests.test_webui_package_facades
|
||||
"$PYTHON" -m pytest -q tests/test_ui_review_program.py "$META_ROOT"/tests/test_devkit_*.py
|
||||
"$PYTHON" -m pytest -q tests/test_focused_phases.py
|
||||
"$PYTHON" -m unittest tests.test_module_package_workflows tests.test_package_registry_release
|
||||
"$PYTHON" -m unittest tests.test_deployment_installer tests.test_webui_release_dependency_retries
|
||||
"$PYTHON" -m pytest -q tests/test_release_meta_source_tag.py tests/test_release_source_tag_batch.py tests/test_release_meta_preparation.py
|
||||
@@ -59,14 +210,20 @@ cd "$META_ROOT"
|
||||
"$PYTHON" -m unittest tests.test_configuration_package_artifacts
|
||||
"$PYTHON" -m unittest tests.test_institutional_governance_journey
|
||||
"$PYTHON" -m unittest tests.test_institutional_service_journey
|
||||
# devkit-phase: tooling end
|
||||
}
|
||||
|
||||
focused_phase_backend() {
|
||||
# devkit-phase: backend begin
|
||||
cd "$ROOT"
|
||||
|
||||
"$PYTHON" - <<'PY'
|
||||
import ast
|
||||
import pathlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
repos_root = pathlib.Path("/mnt/DATA/git")
|
||||
repos_root = pathlib.Path(os.environ["GOVOPLAN_WORKSPACE_ROOT"])
|
||||
roots = [
|
||||
repo / "src"
|
||||
for repo in sorted(repos_root.glob("govoplan*"))
|
||||
@@ -107,107 +264,127 @@ PY
|
||||
"$PYTHON" -m unittest tests.test_ownership_history_migration tests.test_ownership tests.test_ownership_api
|
||||
"$PYTHON" -m unittest tests.test_navigation_preferences tests.test_api_smoke.ApiSmokeTests.test_navigation_separator_layout_survives_system_tenant_and_personal_saves
|
||||
"$PYTHON" -m pytest -q \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_managed_archives.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archive_work.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archive_staging.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_upload_response_batching.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archive_performance.py
|
||||
"${WORKSPACE_ROOT}/govoplan-files/tests/test_managed_archives.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-files/tests/test_archive_work.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-files/tests/test_archive_staging.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-files/tests/test_upload_response_batching.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-files/tests/test_archive_performance.py"
|
||||
"$PYTHON" -m pytest -q \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archive_workers.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archive_inspection_bounds.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archives.py
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-access/tests/test_external_function_mapping_migration.py
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-access/tests
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-templates/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-dataflow/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-workflow-engine/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-workflow/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-views/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-quick-access/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-dashboard/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-postbox/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-portal/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-payments/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-forms/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-forms-runtime/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-cases/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-committee/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-voting/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-approvals/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-identity-trust/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-encryption/tests
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-wiki/tests
|
||||
"${WORKSPACE_ROOT}/govoplan-files/tests/test_archive_workers.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-files/tests/test_archive_inspection_bounds.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-files/tests/test_archives.py"
|
||||
"$PYTHON" -m pytest -q "${WORKSPACE_ROOT}/govoplan-access/tests/test_external_function_mapping_migration.py"
|
||||
"$PYTHON" -m pytest -q "${WORKSPACE_ROOT}/govoplan-access/tests"
|
||||
"$PYTHON" -m pytest -q "${WORKSPACE_ROOT}/govoplan-templates/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-connectors/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-datasources/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-dataflow/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-workflow-engine/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-workflow/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-views/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-quick-access/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-dashboard/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-postbox/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-portal/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-payments/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-forms/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-forms-runtime/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-cases/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-committee/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-voting/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-approvals/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-identity-trust/tests"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-encryption/tests"
|
||||
"$PYTHON" -m pytest -q "${WORKSPACE_ROOT}/govoplan-wiki/tests"
|
||||
"$PYTHON" -m pytest -q \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_approval_gate.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_editor_state_security.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_mail_profile_boundary.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_independent_configuration_repairs.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_incremental_review_persistence.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_reviewed_build_mock.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_delivery_policy_settings.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_synchronous_delivery_policy.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_workerless_recovery.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_imap_batch_integration.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_testbed_claim_recovery.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_campaign_optimistic_concurrency.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_archive_encryption_governance.py \
|
||||
/mnt/DATA/git/govoplan-policy/tests/test_campaign_archive_encryption.py \
|
||||
/mnt/DATA/git/govoplan-policy/tests/test_archive_encryption_api.py
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_approval_gate.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_editor_state_security.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_mail_profile_boundary.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_independent_configuration_repairs.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_incremental_review_persistence.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_reviewed_build_mock.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_delivery_policy_settings.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_synchronous_delivery_policy.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_workerless_recovery.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_imap_batch_integration.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_testbed_claim_recovery.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_campaign_optimistic_concurrency.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-campaign/tests/test_archive_encryption_governance.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-policy/tests/test_campaign_archive_encryption.py" \
|
||||
"${WORKSPACE_ROOT}/govoplan-policy/tests/test_archive_encryption_api.py"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-datasource-composition.py"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-sanctions-screening-composition.py"
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-mail/tests/test_campaign_protocol_authorization.py /mnt/DATA/git/govoplan-mail/tests/test_campaign_imap_batch.py
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-mail/tests
|
||||
"$PYTHON" -m pytest -q "${WORKSPACE_ROOT}/govoplan-mail/tests/test_campaign_protocol_authorization.py" "${WORKSPACE_ROOT}/govoplan-mail/tests/test_campaign_imap_batch.py"
|
||||
"$PYTHON" -m unittest discover -s "${WORKSPACE_ROOT}/govoplan-mail/tests"
|
||||
"$PYTHON" -m unittest tests.test_api_smoke.ApiSmokeTests.test_mailbox_message_listing_reports_total_count
|
||||
"$PYTHON" -m unittest \
|
||||
tests.test_api_smoke.ApiSmokeTests.test_managed_attachment_patterns_preview_build_and_mock_send \
|
||||
tests.test_api_smoke.ApiSmokeTests.test_reports_and_job_review_are_scoped_to_the_selected_version \
|
||||
tests.test_api_smoke.ApiSmokeTests.test_worker_loss_becomes_unknown_and_requires_reconciliation_before_retry
|
||||
# devkit-phase: backend end
|
||||
}
|
||||
|
||||
focused_phase_core_ui() {
|
||||
# devkit-phase: core-ui begin
|
||||
cd "$ROOT/webui"
|
||||
"$NPM" run test:api-client-cache
|
||||
"$NPM" run test:auth-action-state
|
||||
"$NPM" run test:dependency-security
|
||||
"$NPM" run test:layout-primitives
|
||||
"$NPM" run test:mail-components
|
||||
"$NPM" run test:components -- layout-primitives page-layout data-grid-actions mail-components
|
||||
"$NODE" --test tests/breadcrumb-bar.test.mjs
|
||||
"$NPM" run test:module-capabilities
|
||||
"$NPM" run test:module-permutations
|
||||
"$NPM" run test:conformance
|
||||
# devkit-phase: core-ui end
|
||||
}
|
||||
|
||||
cd /mnt/DATA/git/govoplan-access/webui
|
||||
focused_phase_module_builds() {
|
||||
# devkit-phase: module-builds begin
|
||||
cd "$ROOT/webui"
|
||||
"$NPM" run test:module-permutations
|
||||
# devkit-phase: module-builds end
|
||||
}
|
||||
|
||||
focused_phase_browser() {
|
||||
# devkit-phase: browser begin
|
||||
cd "$ROOT/webui"
|
||||
"$NPM" run test:conformance
|
||||
# devkit-phase: browser end
|
||||
}
|
||||
|
||||
focused_phase_module_ui() {
|
||||
# devkit-phase: module-ui begin
|
||||
cd "${WORKSPACE_ROOT}/govoplan-access/webui"
|
||||
"$NPM" run test:passwords
|
||||
|
||||
"$WEBUI_BIN/tsc" -p /mnt/DATA/git/govoplan-payments/webui/tsconfig.json
|
||||
"$WEBUI_BIN/tsc" -p "${WORKSPACE_ROOT}/govoplan-payments/webui/tsconfig.json"
|
||||
|
||||
cd /mnt/DATA/git/govoplan-payments/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-payments/webui"
|
||||
"$NPM" run test:interface-pattern
|
||||
|
||||
cd /mnt/DATA/git/govoplan-dataflow/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-dataflow/webui"
|
||||
"$NPM" run test:structure
|
||||
|
||||
cd /mnt/DATA/git/govoplan-datasources/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-datasources/webui"
|
||||
"$NPM" run typecheck
|
||||
|
||||
cd /mnt/DATA/git/govoplan-workflow/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-workflow/webui"
|
||||
"$NPM" run typecheck
|
||||
|
||||
cd /mnt/DATA/git/govoplan-dashboard/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-dashboard/webui"
|
||||
"$NPM" run test:dashboard-layout
|
||||
|
||||
cd /mnt/DATA/git/govoplan-approvals/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-approvals/webui"
|
||||
"$NPM" run test:workspace-layout
|
||||
|
||||
cd /mnt/DATA/git/govoplan-postbox/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-postbox/webui"
|
||||
"$NPM" run test:ui-structure
|
||||
|
||||
cd /mnt/DATA/git/govoplan-mail/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-mail/webui"
|
||||
"$NPM" run test:mail-ui
|
||||
|
||||
cd /mnt/DATA/git/govoplan-files/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-files/webui"
|
||||
"$NPM" run test:managed-archive
|
||||
|
||||
cd /mnt/DATA/git/govoplan-campaign/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-campaign/webui"
|
||||
"$NPM" run test:policy-ui
|
||||
"$NPM" run test:template-preview
|
||||
"$NPM" run test:review-workflow
|
||||
@@ -215,8 +392,25 @@ cd /mnt/DATA/git/govoplan-campaign/webui
|
||||
"$NPM" run test:campaign-collaboration
|
||||
"$NPM" run test:campaign-work
|
||||
|
||||
cd /mnt/DATA/git/govoplan-policy/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-policy/webui"
|
||||
"$NPM" run test:archive-encryption
|
||||
|
||||
cd /mnt/DATA/git/govoplan-wiki/webui
|
||||
cd "${WORKSPACE_ROOT}/govoplan-wiki/webui"
|
||||
"$NPM" run test:interface-pattern
|
||||
# devkit-phase: module-ui end
|
||||
}
|
||||
|
||||
# Validate every selected implementation before setup can create a temp npmrc.
|
||||
while IFS= read -r phase_id; do
|
||||
if ! declare -F "focused_phase_${phase_id//-/_}" >/dev/null; then
|
||||
echo "check-focused: missing phase implementation: $phase_id" >&2
|
||||
exit 2
|
||||
fi
|
||||
done <<< "$FOCUSED_SELECTION"
|
||||
|
||||
while IFS= read -r phase_id; do
|
||||
(
|
||||
focused_setup
|
||||
"focused_phase_${phase_id//-/_}"
|
||||
)
|
||||
done <<< "$FOCUSED_SELECTION"
|
||||
|
||||
Executable
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
/** UI-01: contextual documentation belongs beside text, never in action slots. */
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const workspaceRoot = resolve(import.meta.dirname, "../../..");
|
||||
const require = createRequire(resolve(workspaceRoot, "govoplan-core/webui/package.json"));
|
||||
const ts = require("typescript");
|
||||
const titleOwners = new Set(["PageLayout", "PageHeader", "PageTitle", "AdminPageLayout", "Card", "Dialog", "PageActionBar", "WorkspaceActionBar"]);
|
||||
const interactiveOwners = new Set(["a", "button", "Button", "IconButton"]);
|
||||
// Existing domain dialog adapter; its owning structural tests must retain
|
||||
// forwarding to Core Dialog.titleHelp (not a module-local heading definition).
|
||||
const domainTitleOwners = new Map([["FileDialog", "/govoplan-files/webui/src/"]]);
|
||||
|
||||
export function findDetachedDocumentation(sources) {
|
||||
const files = new Map(sources.map(({ path, source }) => [resolve(path),
|
||||
ts.createSourceFile(resolve(path), source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)]));
|
||||
const options = { noEmit: true, noResolve: true, noLib: true, types: [], jsx: ts.JsxEmit.Preserve };
|
||||
const host = ts.createCompilerHost(options);
|
||||
host.getSourceFile = (path) => files.get(resolve(path));
|
||||
const checker = ts.createProgram([...files.keys()], options, host).getTypeChecker();
|
||||
const findings = [];
|
||||
let links = 0;
|
||||
for (const [path, source] of files) {
|
||||
const identifiers = [];
|
||||
const helpNodes = [];
|
||||
function importedName(node) {
|
||||
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
|
||||
const namespace = checker.getSymbolAtLocation(node.expression)?.declarations?.find(ts.isNamespaceImport);
|
||||
if (namespace) return node.name.text;
|
||||
}
|
||||
const declarations = checker.getSymbolAtLocation(node)?.declarations ?? [];
|
||||
const declaration = declarations.find(ts.isImportSpecifier);
|
||||
if (declaration) return (declaration.propertyName ?? declaration.name).text;
|
||||
const defaultImport = declarations.find(ts.isImportClause);
|
||||
if (defaultImport && ts.isStringLiteral(defaultImport.parent.moduleSpecifier)) {
|
||||
const component = defaultImport.parent.moduleSpecifier.text.split("/").at(-1).replace(/\.[cm]?[jt]sx?$/, "");
|
||||
if (component === "DocumentationHelpLink" || titleOwners.has(component) || component === "TextWithHelp" || interactiveOwners.has(component)) return component;
|
||||
}
|
||||
return node.getText(source);
|
||||
}
|
||||
function tag(node) {
|
||||
const opening = ts.isJsxElement(node) ? node.openingElement : ts.isJsxSelfClosingElement(node) ? node : null;
|
||||
return opening ? importedName(opening.tagName) : null;
|
||||
}
|
||||
function collect(node) {
|
||||
if (ts.isIdentifier(node)) identifiers.push(node);
|
||||
if ((ts.isJsxSelfClosingElement(node) || ts.isJsxElement(node)) && tag(node) === "DocumentationHelpLink") helpNodes.push(node);
|
||||
ts.forEachChild(node, collect);
|
||||
}
|
||||
collect(source);
|
||||
|
||||
function staticallyHidden(opening) {
|
||||
const hidden = opening.attributes.properties.find((attribute) => ts.isJsxAttribute(attribute) && attribute.name.getText(source) === "hidden");
|
||||
if (!hidden) return false;
|
||||
if (!hidden.initializer || ts.isStringLiteral(hidden.initializer)) return true;
|
||||
return ts.isJsxExpression(hidden.initializer) && hidden.initializer.expression?.kind === ts.SyntaxKind.TrueKeyword;
|
||||
}
|
||||
|
||||
// Reject text that is definitely absent while preserving dynamic translated
|
||||
// titles and components whose rendered text cannot be established statically.
|
||||
function emptyText(node, seen = new Set()) {
|
||||
if (!node) return true;
|
||||
if (ts.isJsxText(node)) return !node.getText(source).trim();
|
||||
if (ts.isJsxExpression(node)) return emptyText(node.expression, seen);
|
||||
if (ts.isStringLiteralLike(node)) return !node.text.trim();
|
||||
if ([ts.SyntaxKind.NullKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.TrueKeyword].includes(node.kind) || ts.isVoidExpression(node)) return true;
|
||||
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) return emptyText(node.expression, seen);
|
||||
if (ts.isIdentifier(node)) {
|
||||
const symbol = checker.getSymbolAtLocation(node);
|
||||
const declaration = symbol?.declarations?.find(ts.isVariableDeclaration);
|
||||
const immutable = declaration && ts.isVariableDeclarationList(declaration.parent) && Boolean(declaration.parent.flags & ts.NodeFlags.Const);
|
||||
if (immutable && declaration.initializer && !seen.has(symbol)) return emptyText(declaration.initializer, new Set(seen).add(symbol));
|
||||
return node.text === "undefined" && !symbol?.declarations?.length;
|
||||
}
|
||||
if (ts.isConditionalExpression(node)) return emptyText(node.whenTrue, seen) && emptyText(node.whenFalse, seen);
|
||||
if (ts.isJsxFragment(node)) return node.children.every((child) => emptyText(child, seen));
|
||||
if (ts.isArrayLiteralExpression(node)) return node.elements.every((child) => emptyText(child, seen));
|
||||
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
||||
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
||||
if (staticallyHidden(opening)) return true;
|
||||
if (/^[a-z]/.test(opening.tagName.getText(source))) return !ts.isJsxElement(node) || node.children.every((child) => emptyText(child, seen));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isAnchored(node, seen = new Set()) {
|
||||
// Check the complete rendered ancestry before returning at a recognized
|
||||
// slot: the entire heading/text contract may itself be inside a button.
|
||||
for (let parent = node.parent; parent; parent = parent.parent) {
|
||||
if (interactiveOwners.has(tag(parent))) return false;
|
||||
const opening = ts.isJsxElement(parent) ? parent.openingElement : ts.isJsxSelfClosingElement(parent) ? parent : null;
|
||||
if (opening && staticallyHidden(opening)) return false;
|
||||
}
|
||||
for (let parent = node.parent; parent; parent = parent.parent) {
|
||||
if (ts.isJsxAttribute(parent)) {
|
||||
const owner = parent.parent.parent;
|
||||
const name = importedName(owner.tagName);
|
||||
const slot = parent.name.getText(source);
|
||||
if (slot === "titleHelp" && (titleOwners.has(name) || (domainTitleOwners.has(name) && path.includes(domainTitleOwners.get(name))))) {
|
||||
if (name === "PageTitle") {
|
||||
const element = ts.isJsxOpeningElement(owner) ? owner.parent : null;
|
||||
return Boolean(element?.children.some((child) => !emptyText(child)));
|
||||
}
|
||||
const title = owner.attributes.properties.find((attribute) => ts.isJsxAttribute(attribute) && attribute.name.getText(source) === "title");
|
||||
return Boolean(title && !emptyText(title.initializer));
|
||||
}
|
||||
if (slot === "help" && name === "TextWithHelp") {
|
||||
const element = ts.isJsxOpeningElement(owner) ? owner.parent : null;
|
||||
return Boolean(element?.children.some((child) => !emptyText(child)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
||||
const symbol = checker.getSymbolAtLocation(parent.name);
|
||||
if (!symbol || seen.has(symbol)) return false;
|
||||
const next = new Set(seen).add(symbol);
|
||||
const references = identifiers.filter((identifier) => identifier !== parent.name && checker.getSymbolAtLocation(identifier) === symbol);
|
||||
return references.length > 0 && references.every((reference) => isAnchored(reference, next));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const node of helpNodes) {
|
||||
links += 1;
|
||||
// FieldLabel is the central label+book implementation; its browser/component
|
||||
// contract verifies sibling text and prevents nested interactive controls.
|
||||
if (path.endsWith("/govoplan-core/webui/src/components/help/FieldLabel.tsx")) continue;
|
||||
if (!isAnchored(node)) {
|
||||
const position = source.getLineAndCharacterOfPosition(node.getStart(source));
|
||||
findings.push({ path, line: position.line + 1, column: position.character + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { findings, links };
|
||||
}
|
||||
|
||||
function sourceFiles(directory) {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = resolve(directory, entry.name);
|
||||
return entry.isDirectory() ? sourceFiles(path) : entry.name.endsWith(".tsx") ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
export function checkWorkspace(root = workspaceRoot) {
|
||||
const modules = readdirSync(root, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith("govoplan"))
|
||||
.map((entry) => resolve(root, entry.name, "webui/src")).filter(existsSync);
|
||||
const paths = modules.flatMap(sourceFiles);
|
||||
const { findings, links } = findDetachedDocumentation(paths.map((path) => ({ path, source: readFileSync(path, "utf8") })));
|
||||
for (const finding of findings) {
|
||||
console.error(`${relative(root, finding.path)}:${finding.line}:${finding.column}: UI-01 documentation must use a heading's titleHelp or TextWithHelp beside visible text, not an action slot or detached row.`);
|
||||
}
|
||||
if (!findings.length) console.log(`Heading-help contract passed: ${links} documentation links in ${paths.length} TSX files across ${modules.length} WebUI modules.`);
|
||||
return findings.length ? 1 : 0;
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = checkWorkspace();
|
||||
@@ -4,6 +4,7 @@ set -euo pipefail
|
||||
META_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
ROOT="${GOVOPLAN_CORE_ROOT:-$META_ROOT/../govoplan-core}"
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
WORKSPACE_ROOT="${GOVOPLAN_WORKSPACE_ROOT:-$(dirname "$ROOT")}"
|
||||
VENV_ROOT="${GOVOPLAN_VENV_ROOT:-$META_ROOT/.venv}"
|
||||
PYTHON="${PYTHON:-$VENV_ROOT/bin/python}"
|
||||
NPM="${NPM:-/home/zemion/.nvm/versions/node/v22.22.3/bin/npm}"
|
||||
@@ -184,6 +185,7 @@ run_step "Validate installed module manifests and registry"
|
||||
|
||||
run_step "Validate platform interface and endpoint declarations"
|
||||
"$PYTHON" "$META_ROOT/tools/inventory/platform-interface-inventory.py" \
|
||||
--workspace-root "$WORKSPACE_ROOT" \
|
||||
--strict-declarations \
|
||||
--strict-endpoints
|
||||
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"phases": [
|
||||
{
|
||||
"id": "preflight",
|
||||
"title": "Dependency and shared contract preflight",
|
||||
"cwd": "core",
|
||||
"order_after": [],
|
||||
"depends_on": [],
|
||||
"resources": [
|
||||
"backend:test-state"
|
||||
],
|
||||
"outputs": [],
|
||||
"notes": [
|
||||
"Checks dependency hygiene, cross-module contracts, manifests and static WebUI conventions."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tooling",
|
||||
"title": "Workspace inventories and development/release tooling tests",
|
||||
"cwd": "meta",
|
||||
"order_after": [
|
||||
"preflight"
|
||||
],
|
||||
"depends_on": [],
|
||||
"resources": [
|
||||
"backend:test-state",
|
||||
"artifact:platform-inventory"
|
||||
],
|
||||
"outputs": [
|
||||
"{meta}/audit-reports/platform-inventory/platform-interface-inventory.json",
|
||||
"{meta}/audit-reports/platform-inventory/platform-interface-inventory.md"
|
||||
],
|
||||
"notes": [
|
||||
"Inventory reports are regenerated here; no later phase consumes them. Reused check evidence does not promise these reports still exist."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "backend",
|
||||
"title": "Workspace syntax, backend modules and integration checks",
|
||||
"cwd": "core",
|
||||
"order_after": [
|
||||
"tooling"
|
||||
],
|
||||
"depends_on": [],
|
||||
"resources": [
|
||||
"backend:test-state"
|
||||
],
|
||||
"outputs": [],
|
||||
"notes": [
|
||||
"Tests may create their own temporary fixtures or test caches; no generated artifact is required by a later phase."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "core-ui",
|
||||
"title": "Core source tests and selected component contracts",
|
||||
"cwd": "core-webui",
|
||||
"order_after": [
|
||||
"backend"
|
||||
],
|
||||
"depends_on": [],
|
||||
"resources": [
|
||||
"webui:govoplan-core"
|
||||
],
|
||||
"outputs": [
|
||||
"{core}/webui/.module-test-build"
|
||||
],
|
||||
"notes": [
|
||||
"The selected component batch creates and removes a private compilation directory. Module-capability tests rebuild .module-test-build themselves; later phases do not consume it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "module-builds",
|
||||
"title": "Optional-module build permutations and bundle checks",
|
||||
"cwd": "core-webui",
|
||||
"order_after": [
|
||||
"core-ui"
|
||||
],
|
||||
"depends_on": [],
|
||||
"resources": [
|
||||
"webui:govoplan-core"
|
||||
],
|
||||
"outputs": [
|
||||
"{core}/webui/dist",
|
||||
"{core}/webui/dist/module-permutation-bundle-metrics.json"
|
||||
],
|
||||
"notes": [
|
||||
"Each permutation builds and reads its own fresh bundle metrics. Final dist and aggregate metrics persist, but browser/module-ui phases do not consume them. Reused check evidence is not artifact or deployment attestation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "browser",
|
||||
"title": "Core browser conformance",
|
||||
"cwd": "core-webui",
|
||||
"order_after": [
|
||||
"module-builds"
|
||||
],
|
||||
"depends_on": [],
|
||||
"resources": [
|
||||
"webui:govoplan-core",
|
||||
"browser:chromium",
|
||||
"port:4174"
|
||||
],
|
||||
"outputs": [
|
||||
"{core}/webui/test-results",
|
||||
"{core}/webui/node_modules/.vite/govoplan-conformance"
|
||||
],
|
||||
"notes": [
|
||||
"Playwright starts its own Vite server from conformance sources with an isolated cache. It does not serve or require module-builds dist. The server is owned by the phase and stopped on completion."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "module-ui",
|
||||
"title": "Owning-module UI and type checks",
|
||||
"cwd": "access-webui",
|
||||
"order_after": [
|
||||
"browser"
|
||||
],
|
||||
"depends_on": [],
|
||||
"resources": [
|
||||
"webui:govoplan-core",
|
||||
"webui:govoplan-access",
|
||||
"webui:govoplan-payments",
|
||||
"webui:govoplan-dataflow",
|
||||
"webui:govoplan-datasources",
|
||||
"webui:govoplan-workflow",
|
||||
"webui:govoplan-dashboard",
|
||||
"webui:govoplan-approvals",
|
||||
"webui:govoplan-postbox",
|
||||
"webui:govoplan-mail",
|
||||
"webui:govoplan-files",
|
||||
"webui:govoplan-campaign",
|
||||
"webui:govoplan-policy",
|
||||
"webui:govoplan-wiki"
|
||||
],
|
||||
"outputs": [],
|
||||
"notes": [
|
||||
"The original 19-command tail retains each owning package's cwd. Its scripts prepare any private test output they need; no earlier phase artifact is a prerequisite."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user