@atmem/architect 0.0.0-stage → 0.2.0-alpha.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONSTITUTION.md +144 -0
- package/README.md +33 -2
- package/agent-skills/discovery/SKILL.md +48 -0
- package/agent-skills/discovery/topics.json +139 -0
- package/agent-skills/generation-capabilities.json +760 -0
- package/agent-skills/service-lifecycle.json +21 -0
- package/agent-skills/specialists/architecture/SKILL.md +15 -0
- package/agent-skills/specialists/aws/SKILL.md +17 -0
- package/agent-skills/specialists/azure/SKILL.md +7 -0
- package/agent-skills/specialists/composition/SKILL.md +17 -0
- package/agent-skills/specialists/devops/SKILL.md +10 -0
- package/agent-skills/specialists/finops/SKILL.md +7 -0
- package/agent-skills/specialists/gcp/SKILL.md +7 -0
- package/agent-skills/specialists/ibm/SKILL.md +7 -0
- package/agent-skills/specialists/manifest.json +13 -0
- package/agent-skills/specialists/pulumi/SKILL.md +7 -0
- package/agent-skills/specialists/security/SKILL.md +7 -0
- package/agent-skills/specialists/terraform/SKILL.md +14 -0
- package/agent-skills/upstream/aws/LICENSE +175 -0
- package/agent-skills/upstream/aws/SKILL.md +125 -0
- package/agent-skills/upstream/azure/LICENSE +21 -0
- package/agent-skills/upstream/azure/SKILL.md +387 -0
- package/agent-skills/upstream/gcp/LICENSE +202 -0
- package/agent-skills/upstream/gcp/SKILL.md +159 -0
- package/agent-skills/upstream/manifest.json +57 -0
- package/agent-skills/upstream/pulumi/LICENSE +201 -0
- package/agent-skills/upstream/pulumi/SKILL.md +501 -0
- package/agent-skills/upstream/terraform/LICENSE +373 -0
- package/agent-skills/upstream/terraform/SKILL.md +316 -0
- package/agent-skills/workflows/workflows.json +601 -0
- package/cli/architect.mjs +3 -0
- package/cli/runtime.mjs +115 -0
- package/dist/assets/html2canvas.esm-QH1iLAAe.js +22 -0
- package/dist/assets/index-B97MkVMs.css +1 -0
- package/dist/assets/index-qtCM2D7O.js +21271 -0
- package/dist/assets/index.es-C6C0sneB.js +18 -0
- package/dist/assets/purify.es-CAi2OJFm.js +3 -0
- package/dist/assets/visual-export-DjbjgQbA.js +173 -0
- package/dist/favicon.svg +2 -0
- package/dist/fonts/DM-Sans-OFL.txt +93 -0
- package/dist/fonts/Manrope-OFL.txt +93 -0
- package/dist/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/fonts/OFL.txt +94 -0
- package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/index.html +4 -0
- package/dist/licenses/aws-architect-LICENSE.txt +17 -0
- package/dist/licenses/aws-mcp-LICENSE.txt +175 -0
- package/dist/licenses/aws-mcp-NOTICE.txt +1 -0
- package/dist/licenses/cloud-emulation-notices.txt +36619 -0
- package/dist/licenses/dependency-notices.txt +6379 -0
- package/dist/licenses/emulation-notices.txt +717 -0
- package/dist/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/dist/licenses/memory-dependency-notices.txt +3319 -0
- package/dist/licenses/openarchflow-LICENSE.txt +21 -0
- package/dist/licenses/python-dependency-notices.txt +11951 -0
- package/docs/upstream-lock.json +26 -0
- package/knowledge/engineering-practices.json +536 -0
- package/package.json +30 -4
- package/packaging/uv-release-lock.json +54 -0
- package/public/licenses/aws-architect-LICENSE.txt +17 -0
- package/public/licenses/aws-mcp-LICENSE.txt +175 -0
- package/public/licenses/aws-mcp-NOTICE.txt +1 -0
- package/public/licenses/cloud-emulation-notices.txt +36619 -0
- package/public/licenses/dependency-notices.txt +6379 -0
- package/public/licenses/emulation-notices.txt +717 -0
- package/public/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/public/licenses/memory-dependency-notices.txt +3319 -0
- package/public/licenses/openarchflow-LICENSE.txt +21 -0
- package/public/licenses/python-dependency-notices.txt +11951 -0
- package/scripts/terraform-render.mjs +8 -0
- package/server/__init__.py +0 -0
- package/server/abstraction.py +28 -0
- package/server/agent_runtime.py +153 -0
- package/server/app.py +257 -0
- package/server/architect.py +112 -0
- package/server/architecture_activity.py +67 -0
- package/server/attachments.py +101 -0
- package/server/cloud-emulation-requirements.txt +29 -0
- package/server/codex_app_server.py +219 -0
- package/server/codex_engineering_backend.py +266 -0
- package/server/codex_worker_policy.py +170 -0
- package/server/contracts.py +315 -0
- package/server/copilot_worker.py +16 -0
- package/server/dependency_mirror.py +54 -0
- package/server/design_patch.py +43 -0
- package/server/emulation-requirements.txt +8 -0
- package/server/emulation_adapters.py +20 -0
- package/server/emulation_routes.py +98 -0
- package/server/emulation_worker.py +351 -0
- package/server/engineering_artifacts.py +215 -0
- package/server/engineering_assets/application-runtime.json +4372 -0
- package/server/engineering_assets/architecture_work.schema.json +79 -0
- package/server/engineering_assets/materialize_runtime.py +62 -0
- package/server/engineering_assets/prepare_tools.py +96 -0
- package/server/engineering_assets/sql_validation.py +59 -0
- package/server/engineering_contexts.py +120 -0
- package/server/engineering_delivery.py +210 -0
- package/server/engineering_finalization.py +210 -0
- package/server/engineering_handoff.py +163 -0
- package/server/engineering_jobs.py +206 -0
- package/server/engineering_knowledge.py +75 -0
- package/server/engineering_reconciliation.py +159 -0
- package/server/engineering_routes.py +469 -0
- package/server/engineering_sessions.py +273 -0
- package/server/engineering_tools.py +166 -0
- package/server/generation_lint.py +18 -0
- package/server/local_providers.py +181 -0
- package/server/local_validation.py +236 -0
- package/server/memory-requirements.lock +181 -0
- package/server/memory-requirements.txt +6 -0
- package/server/memory_routes.py +82 -0
- package/server/memory_service.py +273 -0
- package/server/memory_worker.py +66 -0
- package/server/openai_voice.py +80 -0
- package/server/packaged_app.py +9 -0
- package/server/pricing.py +84 -0
- package/server/process_control_observation.py +31 -0
- package/server/prompts.py +123 -0
- package/server/public_faq_emulation_worker.py +44 -0
- package/server/requirements.lock +1618 -0
- package/server/requirements.txt +68 -0
- package/server/runtime_dependency_locks.py +27 -0
- package/server/runtime_evidence.py +93 -0
- package/server/settings.py +14 -0
- package/server/skills.py +103 -0
- package/server/storage_emulation_worker.py +160 -0
- package/server/structured_emulation_worker.py +48 -0
- package/server/terraform_routes.py +118 -0
- package/server/upstream/__init__.py +0 -0
- package/server/upstream/agent/__init__.py +1 -0
- package/server/upstream/agent/aws_architect.py +600 -0
- package/server/upstream/agent/config/__init__.py +1 -0
- package/server/upstream/agent/config/agent_config.py +82 -0
- package/server/upstream/agent/config/prompts.py +138 -0
- package/server/upstream/agent/rate_limiter.py +62 -0
- package/server/upstream/utils/__init__.py +1 -0
- package/server/upstream/utils/file_writer.py +43 -0
- package/server/validation_applicability.py +85 -0
- package/server/validation_jobs.py +252 -0
- package/server/validation_workers.py +48 -0
- package/server/voice_routes.py +91 -0
- package/server/voice_worker.py +26 -0
- package/specs/028-convergence-evaluation/evidence/20261110-ibm-invoice-native/registry-structured/qualification.json +72 -0
- package/specs/028-convergence-evaluation/evidence/20261110-invoice-runtime/durable-current/receipt.json +61 -0
- package/specs/029-general-purpose-engine/evidence/20261111-runtime-dependencies/sdk/receipt.json +22 -0
- package/src/ArchitectureActivity.jsx +54 -0
- package/src/ArchitectureWorkPanel.jsx +46 -0
- package/src/ArtifactChecks.jsx +49 -0
- package/src/BlockRequirements.jsx +13 -0
- package/src/BrainActivity.jsx +11 -0
- package/src/ChatMessage.jsx +19 -0
- package/src/ChatResizeHandle.jsx +6 -0
- package/src/CodePreview.jsx +8 -0
- package/src/ComponentReview.jsx +31 -0
- package/src/DeliveryViews.jsx +59 -0
- package/src/EmulationPanel.jsx +44 -0
- package/src/EngagementViews.jsx +385 -0
- package/src/EngineeringPanel.jsx +151 -0
- package/src/ImplementationDelivery.jsx +55 -0
- package/src/JourneyViews.jsx +400 -0
- package/src/MemoryPanel.jsx +46 -0
- package/src/OpenAIVoicePanel.jsx +65 -0
- package/src/OutputsView.jsx +65 -0
- package/src/TerraformView.jsx +57 -0
- package/src/WorkbenchDialogs.jsx +532 -0
- package/src/activity-presentation.mjs +28 -0
- package/src/ai-qualification-scenarios.mjs +31 -0
- package/src/approval-application-files.mjs +32 -0
- package/src/approval-applications.mjs +82 -0
- package/src/approval-spec-kit.mjs +30 -0
- package/src/architecture-activity.css +28 -0
- package/src/architecture-activity.mjs +63 -0
- package/src/architecture-work.mjs +176 -0
- package/src/artifact-work.mjs +103 -0
- package/src/async-documents.mjs +191 -0
- package/src/atflow.css +1140 -0
- package/src/atforma.css +748 -0
- package/src/atmem-tokens.css +79 -0
- package/src/aws-preflight.mjs +60 -0
- package/src/aws-public-faq.mjs +102 -0
- package/src/aws-validation-scenarios.mjs +10 -0
- package/src/aws-validation.mjs +48 -0
- package/src/azure-plan-mocks.mjs +25 -0
- package/src/azure-structured.mjs +133 -0
- package/src/bedrock-summary.mjs +152 -0
- package/src/bounded-rag.mjs +60 -0
- package/src/brand.css +9 -0
- package/src/canvas-view.mjs +28 -0
- package/src/capability-composition.mjs +44 -0
- package/src/chat-attachments.mjs +13 -0
- package/src/cloud-provider-locks.mjs +9 -0
- package/src/cloud-storage-scenarios.mjs +2 -0
- package/src/component-review.mjs +55 -0
- package/src/composition-spec-kit.mjs +30 -0
- package/src/consultation-context.mjs +15 -0
- package/src/consultation.mjs +52 -0
- package/src/container-contracts.mjs +5 -0
- package/src/container-inspection.mjs +75 -0
- package/src/container-runtime.mjs +30 -0
- package/src/container-security-inspection.mjs +49 -0
- package/src/container-terraform.mjs +109 -0
- package/src/conversation.mjs +17 -0
- package/src/core.mjs +863 -0
- package/src/cross-cloud-storage.mjs +100 -0
- package/src/delivery-capabilities.mjs +25 -0
- package/src/delivery.mjs +58 -0
- package/src/deployment-ownership.mjs +10 -0
- package/src/deployment-region.mjs +11 -0
- package/src/derive.mjs +31 -0
- package/src/design-build-flow.mjs +10 -0
- package/src/diagram-attention.mjs +24 -0
- package/src/discovery.mjs +359 -0
- package/src/document.mjs +170 -0
- package/src/emulation-feedback.mjs +61 -0
- package/src/emulation-guidance.mjs +37 -0
- package/src/engagement.mjs +274 -0
- package/src/engineering-client.mjs +73 -0
- package/src/engineering-defaults.mjs +17 -0
- package/src/engineering.css +31 -0
- package/src/enterprise-application.mjs +76 -0
- package/src/enterprise-aws-host-infrastructure.mjs +62 -0
- package/src/enterprise-aws-host-tests.mjs +15 -0
- package/src/enterprise-aws-infrastructure-tests.mjs +38 -0
- package/src/enterprise-aws-infrastructure.mjs +138 -0
- package/src/enterprise-google-infrastructure.mjs +84 -0
- package/src/enterprise-google-tests.mjs +25 -0
- package/src/estimate.mjs +188 -0
- package/src/existing-references.mjs +18 -0
- package/src/external-identities.mjs +17 -0
- package/src/faq-lambda-build.mjs +28 -0
- package/src/gcp-structured.mjs +104 -0
- package/src/generation-provider.mjs +7 -0
- package/src/generation-quality.mjs +69 -0
- package/src/handoff.mjs +141 -0
- package/src/ibm-registry.mjs +12 -0
- package/src/ibm-structured.mjs +103 -0
- package/src/identifiers.mjs +15 -0
- package/src/implementation-map.mjs +14 -0
- package/src/implementation-outcome.mjs +8 -0
- package/src/incomplete-terraform.mjs +9 -0
- package/src/integrations.ts +137 -0
- package/src/interface-contracts.mjs +30 -0
- package/src/invoice-application.mjs +66 -0
- package/src/invoice-aws-operations.mjs +136 -0
- package/src/invoice-aws.mjs +143 -0
- package/src/invoice-azure-operations.mjs +147 -0
- package/src/invoice-azure.mjs +134 -0
- package/src/invoice-builder.mjs +35 -0
- package/src/invoice-dependencies.mjs +43 -0
- package/src/invoice-gcp-operations.mjs +178 -0
- package/src/invoice-gcp.mjs +104 -0
- package/src/invoice-ibm-operations.mjs +278 -0
- package/src/invoice-ibm.mjs +142 -0
- package/src/invoice-operations.mjs +143 -0
- package/src/invoice-plan.mjs +45 -0
- package/src/invoice-region.mjs +6 -0
- package/src/invoice-spec-kit.mjs +61 -0
- package/src/invoice-state.mjs +7 -0
- package/src/invoice-terraform.mjs +29 -0
- package/src/invoice-tests.mjs +122 -0
- package/src/journey-workspace.css +72 -0
- package/src/journey.mjs +87 -0
- package/src/levels.mjs +145 -0
- package/src/local-agent.mjs +184 -0
- package/src/main.jsx +2458 -0
- package/src/model-policy-preflight.mjs +34 -0
- package/src/openai-voice.mjs +102 -0
- package/src/output-bundle.mjs +11 -0
- package/src/ownership-inspection.mjs +28 -0
- package/src/pack.mjs +201 -0
- package/src/pricing.mjs +31 -0
- package/src/production-state.mjs +8 -0
- package/src/project-copy.mjs +10 -0
- package/src/project-file.mjs +160 -0
- package/src/project-links.mjs +3 -0
- package/src/protected-state.mjs +43 -0
- package/src/public-faq-builder.mjs +19 -0
- package/src/public-faq-clouds.mjs +251 -0
- package/src/public-faq.mjs +122 -0
- package/src/pulumi.mjs +123 -0
- package/src/python-container.mjs +25 -0
- package/src/python-runtime.mjs +50 -0
- package/src/rag-handler.mjs +43 -0
- package/src/rag-web-assets.mjs +52 -0
- package/src/rag-web.mjs +68 -0
- package/src/records.mjs +177 -0
- package/src/resources.mjs +163 -0
- package/src/review.mjs +252 -0
- package/src/runtime/EnterpriseDocument.java +89 -0
- package/src/runtime/approved_http_destination.py +72 -0
- package/src/runtime/aws-lambda-qualification-lock.json +7 -0
- package/src/runtime/aws-python-lock.json +82 -0
- package/src/runtime/aws-web-python-lock.json +887 -0
- package/src/runtime/aws_json_model.py +42 -0
- package/src/runtime/azure-python-lock.json +863 -0
- package/src/runtime/azure-sources.json +8 -0
- package/src/runtime/azure-test-lock.json +23 -0
- package/src/runtime/azure_acceptance.py +192 -0
- package/src/runtime/azure_structured.py +153 -0
- package/src/runtime/bounded_rag.py +126 -0
- package/src/runtime/catalog_admin.py +70 -0
- package/src/runtime/cloud-sql-proxy-lock.json +8 -0
- package/src/runtime/cloud-sql-proxy-security-review.json +30 -0
- package/src/runtime/connector_cli.py +50 -0
- package/src/runtime/connector_sync.py +70 -0
- package/src/runtime/durable-python-lock.json +247 -0
- package/src/runtime/durable_workflow.py +325 -0
- package/src/runtime/enterprise-application-manifest.json +110 -0
- package/src/runtime/enterprise-application-sources.json +266 -0
- package/src/runtime/enterprise-auth-python-lock.json +592 -0
- package/src/runtime/enterprise-aws-host-bootstrap-source.json +1 -0
- package/src/runtime/enterprise-aws-host-probe-source.json +1 -0
- package/src/runtime/enterprise-aws-task-template.json +235 -0
- package/src/runtime/enterprise-document-lock.json +18 -0
- package/src/runtime/enterprise-document.Dockerfile +15 -0
- package/src/runtime/enterprise-runtime-aws-python-lock.json +1115 -0
- package/src/runtime/enterprise-runtime-azure-python-lock.json +1106 -0
- package/src/runtime/enterprise-runtime-gcp-python-lock.json +1060 -0
- package/src/runtime/enterprise-runtime-ibm-python-lock.json +1096 -0
- package/src/runtime/enterprise-runtime-python-lock.json +1058 -0
- package/src/runtime/enterprise-runtime.Dockerfile +15 -0
- package/src/runtime/enterprise-web-app.js +1 -0
- package/src/runtime/enterprise-web-entry.js +105 -0
- package/src/runtime/enterprise-web-lock.json +29 -0
- package/src/runtime/enterprise-web-notices.txt +229 -0
- package/src/runtime/enterprise-web.css +4 -0
- package/src/runtime/enterprise-web.html +8 -0
- package/src/runtime/enterprise_audit.py +91 -0
- package/src/runtime/enterprise_audit_schema.sql +25 -0
- package/src/runtime/enterprise_auth_schema.sql +67 -0
- package/src/runtime/enterprise_auth_store.py +241 -0
- package/src/runtime/enterprise_aws_host_bootstrap.py +120 -0
- package/src/runtime/enterprise_aws_host_probe.py +105 -0
- package/src/runtime/enterprise_aws_hosts.py +136 -0
- package/src/runtime/enterprise_aws_jobs.py +300 -0
- package/src/runtime/enterprise_aws_objects.py +93 -0
- package/src/runtime/enterprise_aws_transfer.py +83 -0
- package/src/runtime/enterprise_azure_batch_api.py +128 -0
- package/src/runtime/enterprise_azure_host.py +599 -0
- package/src/runtime/enterprise_azure_jobs.py +294 -0
- package/src/runtime/enterprise_azure_objects.py +234 -0
- package/src/runtime/enterprise_azure_pool.py +161 -0
- package/src/runtime/enterprise_azure_transfer.py +122 -0
- package/src/runtime/enterprise_configuration.py +136 -0
- package/src/runtime/enterprise_document.py +116 -0
- package/src/runtime/enterprise_document_cli.py +32 -0
- package/src/runtime/enterprise_document_contract.py +54 -0
- package/src/runtime/enterprise_document_job.py +109 -0
- package/src/runtime/enterprise_document_jobs.py +122 -0
- package/src/runtime/enterprise_document_jobs_schema.sql +23 -0
- package/src/runtime/enterprise_extraction_cache.py +186 -0
- package/src/runtime/enterprise_extraction_schema.sql +17 -0
- package/src/runtime/enterprise_google_jobs.py +275 -0
- package/src/runtime/enterprise_google_transfer.py +85 -0
- package/src/runtime/enterprise_http.py +212 -0
- package/src/runtime/enterprise_identity.py +52 -0
- package/src/runtime/enterprise_knowledge.py +152 -0
- package/src/runtime/enterprise_models.py +94 -0
- package/src/runtime/enterprise_native.py +181 -0
- package/src/runtime/enterprise_oauth.py +132 -0
- package/src/runtime/enterprise_operator.py +160 -0
- package/src/runtime/enterprise_parser_service.py +136 -0
- package/src/runtime/enterprise_parser_transport.py +68 -0
- package/src/runtime/enterprise_queries.py +255 -0
- package/src/runtime/enterprise_queries_schema.sql +27 -0
- package/src/runtime/enterprise_query_service.py +111 -0
- package/src/runtime/enterprise_secrets.py +110 -0
- package/src/runtime/enterprise_sessions.py +95 -0
- package/src/runtime/enterprise_sources.py +260 -0
- package/src/runtime/enterprise_web.py +39 -0
- package/src/runtime/enterprise_worker.py +68 -0
- package/src/runtime/export_contract_acceptance.py +18 -0
- package/src/runtime/faq-output-contract.json +40 -0
- package/src/runtime/faq-web-app.js +41 -0
- package/src/runtime/faq-web.css +1 -0
- package/src/runtime/faq-web.html +6 -0
- package/src/runtime/gcp-sources.json +9 -0
- package/src/runtime/gcp_acceptance.py +110 -0
- package/src/runtime/gcp_structured.py +150 -0
- package/src/runtime/http_acceptance.py +39 -0
- package/src/runtime/iap-web-app.js +36 -0
- package/src/runtime/iap-web-source.json +1 -0
- package/src/runtime/ibm-python-lock.json +856 -0
- package/src/runtime/ibm-sources.json +9 -0
- package/src/runtime/ibm_acceptance.py +133 -0
- package/src/runtime/ibm_structured.py +113 -0
- package/src/runtime/invoice-azapi-provider-lock.json +7 -0
- package/src/runtime/invoice-ibm-provider-lock.json +7 -0
- package/src/runtime/invoice-parser-lock.json +1199 -0
- package/src/runtime/invoice-parser-python-lock.json +56 -0
- package/src/runtime/invoice-parser.Dockerfile +190 -0
- package/src/runtime/invoice-sources.json +58 -0
- package/src/runtime/invoice-test-sources.json +24 -0
- package/src/runtime/invoice-web.css +2 -0
- package/src/runtime/invoice-web.html +12 -0
- package/src/runtime/invoice-web.js +112 -0
- package/src/runtime/invoice_configuration.py +17 -0
- package/src/runtime/invoice_document.py +95 -0
- package/src/runtime/invoice_extraction.py +96 -0
- package/src/runtime/invoice_http.py +129 -0
- package/src/runtime/invoice_identity.py +5 -0
- package/src/runtime/invoice_maintenance.py +40 -0
- package/src/runtime/invoice_native.py +77 -0
- package/src/runtime/invoice_object_store.py +153 -0
- package/src/runtime/invoice_operations.py +108 -0
- package/src/runtime/invoice_operator.py +79 -0
- package/src/runtime/invoice_operator_core.py +39 -0
- package/src/runtime/invoice_operator_job.py +147 -0
- package/src/runtime/invoice_parser_cli.py +17 -0
- package/src/runtime/invoice_parser_identity.py +39 -0
- package/src/runtime/invoice_parser_service.py +89 -0
- package/src/runtime/invoice_parser_transport.py +89 -0
- package/src/runtime/invoice_processing.py +24 -0
- package/src/runtime/invoice_retention.py +16 -0
- package/src/runtime/invoice_runtime_secrets.py +43 -0
- package/src/runtime/invoice_stage.py +80 -0
- package/src/runtime/invoice_web.py +39 -0
- package/src/runtime/invoice_worker.py +50 -0
- package/src/runtime/lambda_faq.py +5 -0
- package/src/runtime/parser-requirements.lock +4 -0
- package/src/runtime/policy_review.py +18 -0
- package/src/runtime/postgres-qualification-lock.json +21 -0
- package/src/runtime/public-faq-sources.json +22 -0
- package/src/runtime/public_catalog_admin.py +122 -0
- package/src/runtime/public_catalog_store.py +125 -0
- package/src/runtime/public_faq.py +144 -0
- package/src/runtime/public_faq_app.py +49 -0
- package/src/runtime/python-container-lock.json +18 -0
- package/src/runtime/rag-sources.json +10 -0
- package/src/runtime/rag_acceptance.py +60 -0
- package/src/runtime/rag_aws_adapter.py +95 -0
- package/src/runtime/rds-ca-source.json +9 -0
- package/src/runtime/rds-global-ca.pem +2888 -0
- package/src/runtime/state-contract-check-source.json +4 -0
- package/src/runtime/state_contract_check.py +338 -0
- package/src/runtime/structured-source.json +1 -0
- package/src/runtime/structured-web-app.js +71 -0
- package/src/runtime/structured-web-source.json +1 -0
- package/src/runtime/structured_http.py +105 -0
- package/src/runtime/structured_output.py +48 -0
- package/src/runtime/web-python-lock.json +802 -0
- package/src/runtime/web_user_admin.py +44 -0
- package/src/runtime/workflow_application.py +62 -0
- package/src/runtime/workflow_bindings.py +128 -0
- package/src/runtime/workflow_configuration.py +40 -0
- package/src/runtime/workflow_connection.py +105 -0
- package/src/runtime/workflow_contract.py +54 -0
- package/src/runtime/workflow_database.py +94 -0
- package/src/runtime/workflow_errors.py +6 -0
- package/src/runtime/workflow_http.py +178 -0
- package/src/runtime/workflow_identity.py +69 -0
- package/src/runtime/workflow_payload.py +118 -0
- package/src/runtime/workflow_process_stage.py +88 -0
- package/src/runtime/workflow_profile.py +91 -0
- package/src/runtime/workflow_review.py +106 -0
- package/src/runtime/workflow_role_bootstrap.py +104 -0
- package/src/runtime/workflow_schema.sql +72 -0
- package/src/runtime/workflow_worker.py +142 -0
- package/src/runtime-settings.mjs +15 -0
- package/src/s3-access.mjs +14 -0
- package/src/source-contracts.mjs +17 -0
- package/src/spec-kit.mjs +186 -0
- package/src/state-custody.mjs +32 -0
- package/src/state-inspection.mjs +45 -0
- package/src/state-spec-kit.mjs +13 -0
- package/src/storage-tests.mjs +25 -0
- package/src/structured-api.mjs +133 -0
- package/src/structured-contract.mjs +19 -0
- package/src/structured-operation-policy.mjs +2 -0
- package/src/structured-web.mjs +17 -0
- package/src/style.css +1 -0
- package/src/system-description.mjs +26 -0
- package/src/terraform-admission.mjs +66 -0
- package/src/terraform-composition.mjs +83 -0
- package/src/terraform-expressions.mjs +46 -0
- package/src/terraform-validation-coverage.mjs +11 -0
- package/src/terraform.mjs +128 -0
- package/src/text-statistics.mjs +51 -0
- package/src/upstream/openarchflow/format-payload.ts +26 -0
- package/src/upstream/openarchflow/import/mermaid.ts +205 -0
- package/src/upstream/openarchflow/import/sql-ddl.ts +109 -0
- package/src/upstream/openarchflow/layout.ts +54 -0
- package/src/upstream/openarchflow/providers/aws.ts +196 -0
- package/src/upstream/openarchflow/providers/azure.ts +126 -0
- package/src/upstream/openarchflow/providers/cloudnative.ts +71 -0
- package/src/upstream/openarchflow/providers/generic.ts +227 -0
- package/src/upstream/openarchflow/providers/integration.ts +22 -0
- package/src/upstream/openarchflow/providers/types.ts +2 -0
- package/src/upstream/openarchflow/templates.ts +937 -0
- package/src/upstream-catalog.mjs +10 -0
- package/src/useChatScroll.jsx +36 -0
- package/src/useEmulation.jsx +36 -0
- package/src/validation-receipt.mjs +10 -0
- package/src/visual-export.mjs +92 -0
- package/src/voice-conversation.mjs +117 -0
- package/src/work-intake.mjs +24 -0
- package/src/workflow-runtime-inventory.mjs +3 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function awsDocumentInfrastructureTests(n,seed,region){
|
|
2
|
+
const name=`format("doc-%s",substr(sha256(${JSON.stringify(seed)}),0,12))`,host=`format("doc-host-%s",substr(sha256(${JSON.stringify(seed)}),0,12))`;
|
|
3
|
+
const block=(kind,target,values)=>`${kind} {\n ${kind==='override_resource'?'override_during = plan\n ':''}target = ${target}\n values = ${values}\n}\n`;
|
|
4
|
+
const run=(name,body)=>`run "${name}" {\n command = plan\n ${body}\n}\n`,assert=(condition,message)=>`assert {\n condition = ${condition}\n error_message = ${JSON.stringify(message)}\n}`;
|
|
5
|
+
let text='mock_provider "aws" {}\n';
|
|
6
|
+
for(const target of [`data.aws_iam_role.${n}_worker`,`module.host_verification.data.aws_iam_role.${n}_worker`])text+=block('override_data',target,'{arn="arn:aws:iam::123456789012:role/question-worker",name="question-worker"}');
|
|
7
|
+
text+=block('override_data',`data.aws_ssm_parameter.${n}_recommended_ami`,'{value="ami-0123456789abcdef0"}');
|
|
8
|
+
text+=block('override_data',`data.aws_ami.${n}_host`,'{id="ami-0123456789abcdef0",root_device_name="/dev/xvda"}');
|
|
9
|
+
text+=block('override_resource',`aws_ecs_cluster.${n}`,`{arn=format("arn:aws:ecs:${region}:123456789012:cluster/%s",${name})}`);
|
|
10
|
+
const bucket=`format("atmem-doc-%s",substr(sha256(format("%s:%s","123456789012",${name})),0,20))`;
|
|
11
|
+
text+=block('override_resource',`aws_s3_bucket.${n}`,`{id=${bucket},arn=format("arn:aws:s3:::%s",${bucket})}`);
|
|
12
|
+
for(const purpose of ['parser','transfer'])text+=block('override_resource',`aws_ecr_repository.${n}_${purpose}`,`{arn=format("arn:aws:ecr:${region}:123456789012:repository/%s/${purpose}",${name}),repository_url=format("123456789012.dkr.ecr.${region}.amazonaws.com/%s/${purpose}",${name})}`);
|
|
13
|
+
for(const purpose of ['host','execution','transfer'])text+=block('override_resource',`aws_iam_role.${n}_${purpose}`,`{arn=format("arn:aws:iam::123456789012:role/%s-${purpose}",${name})}`);
|
|
14
|
+
text+=block('override_resource',`aws_ecs_task_definition.${n}`,`{arn=format("arn:aws:ecs:${region}:123456789012:task-definition/document-%s-parser:1",${name}),revision=1}`);
|
|
15
|
+
text+=block('override_resource',`aws_iam_instance_profile.${n}`,`{arn=format("arn:aws:iam::123456789012:instance-profile/%s-host",${name})}`);
|
|
16
|
+
text+=block('override_resource',`aws_vpc.${n}`,'{id="vpc-aaaaaaaaaaaaaaaaa"}');
|
|
17
|
+
for(const [suffix,id]of [['_host','bbbbbbbbbbbbbbbbb'],['_endpoint','ccccccccccccccccc']])text+=block('override_resource',`aws_security_group.${n}${suffix}`,`{id="sg-${id}"}`);
|
|
18
|
+
for(let i=0;i<2;i++){
|
|
19
|
+
text+=block('override_resource',`aws_subnet.${n}_${i}`,`{id="subnet-${String(i+1).repeat(17)}"}`);
|
|
20
|
+
text+=block('override_resource',`aws_route_table.${n}_${i}`,`{id="rtb-${String(i+3).repeat(17)}"}`);
|
|
21
|
+
}
|
|
22
|
+
for(const [index,service]of ['ecs','ecs_agent','ecs_telemetry','ecr_api','ecr_dkr','ssm','ssmmessages','s3'].entries())text+=block('override_resource',`aws_vpc_endpoint.${n}_${service}`,`{id="vpce-${String(index+1).repeat(17)}"${service==='s3'?',prefix_list_id="pl-aaaaaaaaaaaaaaaaa"':''}}`);
|
|
23
|
+
text+=block('override_resource',`aws_launch_template.${n}`,'{id="lt-aaaaaaaaaaaaaaaaa",latest_version=1}');
|
|
24
|
+
text+=block('override_resource',`module.host_verification.aws_ssm_document.${n}`,`{arn=format("arn:aws:ssm:${region}:123456789012:document/%s",${host}),hash="${'f'.repeat(64)}",hash_type="Sha256",latest_version=1}`);
|
|
25
|
+
text+=`variables {\n review_approved=true\n account_id="123456789012"\n worker_role_arn="arn:aws:iam::123456789012:role/question-worker"\n availability_zones=["${region}a","${region}b"]\n}\n`;
|
|
26
|
+
const ready=`variables {\n images_ready=true\n hosts_enabled=true\n host_ami_id="ami-0123456789abcdef0"\n parser_revision="${'c'.repeat(64)}"\n parser_image=format("123456789012.dkr.ecr.${region}.amazonaws.com/%s/parser@sha256:${'a'.repeat(64)}",${name})\n transfer_image=format("123456789012.dkr.ecr.${region}.amazonaws.com/%s/transfer@sha256:${'b'.repeat(64)}",${name})\n}\n`;
|
|
27
|
+
text+=run('foundation_has_no_ready_runtime',assert(`output.document_job_configuration == null && length(aws_ecs_task_definition.${n}) == 0 && length(aws_launch_template.${n}) == 0 && length(aws_autoscaling_group.${n}) == 0`,'No synthetic task, AMI or ready worker during foundation.'));
|
|
28
|
+
text+=run('private_document_substrate',ready+assert(`output.document_executor_configuration.provider == "aws" && output.document_executor_configuration.parserRevision == var.parser_revision && output.document_executor_configuration.configuration == output.document_job_configuration && output.document_job_configuration.format == "enterprise-aws-document-job-v2" && output.document_job_configuration.taskDefinitionArn == aws_ecs_task_definition.${n}[0].arn && output.document_job_configuration.workerRoleArn == var.worker_role_arn`,'Bind the exact static revision and existing worker.')+'\n'+assert(`aws_ecs_task_definition.${n}[0].network_mode == "bridge" && jsondecode(aws_ecs_task_definition.${n}[0].container_definitions)[1].disableNetworking && length(jsondecode(aws_ecs_task_definition.${n}[0].container_definitions)) == 3`,'Keep isolated parser and sequential transfer stages.'));
|
|
29
|
+
text+=run('private_host_bootstrap',ready+assert(`aws_launch_template.${n}[0].metadata_options[0].http_tokens == "required" && aws_launch_template.${n}[0].metadata_options[0].http_put_response_hop_limit == 1 && !one(aws_launch_template.${n}[0].network_interfaces).associate_public_ip_address && one(aws_launch_template.${n}[0].block_device_mappings).ebs[0].encrypted`,'Private host identity and encrypted disposable root volume.')+'\n'+assert(`aws_autoscaling_group.${n}[0].desired_capacity == var.host_count && aws_autoscaling_group.${n}[0].max_size == var.host_count`,'Standing bounded pool; do not claim automatic scaling.'));
|
|
30
|
+
text+=run('storage_has_no_history',assert(`aws_s3_bucket_versioning.${n}.versioning_configuration[0].status == "Disabled" && aws_s3_bucket_public_access_block.${n}.block_public_policy && !aws_s3_bucket.${n}.force_destroy`,'No retained history/public access or destructive populated-store deletion.'));
|
|
31
|
+
text+=run('review_required',`variables {review_approved=false}\n expect_failures=[terraform_data.${n}_review]`);
|
|
32
|
+
text+=run('worker_path_cannot_redirect',`variables {worker_role_arn="arn:aws:iam::123456789012:role/wrong/question-worker"}\n expect_failures=[terraform_data.${n}_review]`);
|
|
33
|
+
text+=run('region_drift_rejected',`variables {region="us-east-1"}\n expect_failures=[var.region]`);
|
|
34
|
+
text+=run('same_zone_rejected',`variables {availability_zones=["${region}a","${region}a"]}\n expect_failures=[var.availability_zones]`);
|
|
35
|
+
text+=run('missing_ami_rejected',`variables {hosts_enabled=true}\n expect_failures=[var.host_ami_id]`);
|
|
36
|
+
text+=run('missing_image_revision_rejected',`variables {images_ready=true}\n expect_failures=[var.parser_image,var.transfer_image,var.parser_revision]`);
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {awsHostVerificationTerraform}from'./enterprise-aws-host-infrastructure.mjs';
|
|
2
|
+
import template from'./runtime/enterprise-aws-task-template.json'with{type:'json'};
|
|
3
|
+
import probe from'./runtime/enterprise-aws-host-probe-source.json'with{type:'json'};
|
|
4
|
+
import bootstrap from'./runtime/enterprise-aws-host-bootstrap-source.json'with{type:'json'};
|
|
5
|
+
import {awsDocumentInfrastructureTests}from'./enterprise-aws-infrastructure-tests.mjs';
|
|
6
|
+
import {enterpriseApplicationFiles}from'./enterprise-application.mjs';
|
|
7
|
+
|
|
8
|
+
// Parser execution substrate. Full private API/worker/SQL/source/model composition
|
|
9
|
+
// and destination qualification remain separate mandatory work, not implied here.
|
|
10
|
+
export function awsDocumentJobTerraform(project,row,environment='staging'){
|
|
11
|
+
const host=awsHostVerificationTerraform(project,row,environment),child=JSON.parse(host['main.tf.json']);
|
|
12
|
+
delete child.provider; // Inherit the root provider, including native test mocks.
|
|
13
|
+
const regionInput=structuredClone(child.variable.region);
|
|
14
|
+
const files={'modules/host-verification/main.tf.json':JSON.stringify(child,null,2)+'\n'};
|
|
15
|
+
const n=row.address,v=key=>'${var.'+key+'}',r=(type,suffix,field)=>'${'+type+'.'+n+suffix+'.'+field+'}',l=key=>'${local.'+key+'}';
|
|
16
|
+
const literal=value=>value.replace(/\$\{/g,()=>'$${').replace(/%\{/g,()=>'%%{');
|
|
17
|
+
const seed='https://atmem.ai/workspaces/'+encodeURIComponent(project.id)+'/'+n+'/'+environment;
|
|
18
|
+
const variable={account_id:child.variable.account_id,region:regionInput,worker_role_arn:child.variable.worker_role_arn,
|
|
19
|
+
review_approved:{type:'bool',default:false,nullable:false,description:'Review destination, ownership, endpoint/host costs, data retention and operating responsibilities before apply.'},
|
|
20
|
+
images_ready:{type:'bool',default:false,nullable:false,description:'Create the task revision only after both real immutable owned artifacts pass their checks.'},
|
|
21
|
+
hosts_enabled:{type:'bool',default:false,nullable:false,description:'Launch new dedicated instances only after the actual AMI and bootstrap have been reviewed.'},
|
|
22
|
+
host_count:{type:'number',default:2,nullable:false,description:'Standing capacity proposal, not autoscaling or a throughput guarantee. Review costs and load evidence.',validation:{condition:'${var.host_count == floor(var.host_count) && var.host_count >= 1 && var.host_count <= 4}',error_message:'Use one to four reviewed dedicated hosts.'}},
|
|
23
|
+
availability_zones:{type:'list(string)',nullable:false,description:'Two distinct destination availability zones in the selected region.',validation:{condition:'${length(var.availability_zones) == 2 && length(distinct(var.availability_zones)) == 2 && alltrue([for zone in var.availability_zones : can(regex("^'+row.region+'[a-z]$",zone))])}',error_message:'Supply two distinct availability zones in the selected region.'}},
|
|
24
|
+
host_ami_id:{type:'string',default:'',nullable:false,description:'Pin the reviewed actual Amazon ECS-optimized AL2023 x86_64 AMI. Recommended metadata is discovery, not approval.',validation:{condition:'${!var.hosts_enabled || can(regex("^ami-[0-9a-f]{17}$",var.host_ami_id))}',error_message:'Pin the actual reviewed AMI before launching hosts.'}},
|
|
25
|
+
parser_revision:{type:'string',default:'',nullable:false,description:'Actual independently qualified parser artifact revision.',validation:{condition:'${!var.images_ready || can(regex("^[0-9a-f]{64}$",var.parser_revision))}',error_message:'Supply the actual qualified parser revision.'}}};
|
|
26
|
+
const examples={review_approved:false,images_ready:false,hosts_enabled:false,account_id:'123456789012',region:row.region,worker_role_arn:'arn:aws:iam::123456789012:role/question-worker',availability_zones:[row.region+'a',row.region+'b'],host_ami_id:'',host_count:2,parser_revision:''};
|
|
27
|
+
for(const purpose of ['parser','transfer']){
|
|
28
|
+
variable[purpose+'_image']={type:'string',default:'',nullable:false,description:'Actual owned regional ECR '+purpose+' image pinned by SHA256.',validation:{condition:'${!var.images_ready || can(regex("^[a-z0-9./_-]+@sha256:[0-9a-f]{64}$",var.'+purpose+'_image))}',error_message:'Use the actual immutable '+purpose+' image.'}};
|
|
29
|
+
examples[purpose+'_image']='';
|
|
30
|
+
}
|
|
31
|
+
const locals={name:'${format("doc-%s",substr(sha256('+JSON.stringify(seed)+'),0,12))}',
|
|
32
|
+
cluster_arn:r('aws_ecs_cluster','','arn'),host_policy:'${module.host_verification.host_verification.hostPolicySha256}',
|
|
33
|
+
expected:{format:'enterprise-aws-host-policy-v1',clusterArn:l('cluster_arn'),policySha256:l('host_policy')}};
|
|
34
|
+
const resource={},data={aws_iam_role:{[n+'_worker']:{name:'${basename(var.worker_role_arn)}'}},
|
|
35
|
+
aws_ssm_parameter:{[n+'_recommended_ami']:{name:'/aws/service/ecs/optimized-ami/amazon-linux-2023/recommended/image_id',with_decryption:false}},
|
|
36
|
+
aws_ami:{[n+'_host']:{count:'${var.hosts_enabled ? 1 : 0}',owners:['amazon'],filter:[{name:'image-id',values:[v('host_ami_id')]},{name:'architecture',values:['x86_64']},{name:'virtualization-type',values:['hvm']},{name:'root-device-type',values:['ebs']},{name:'name',values:['al2023-ami-ecs-hvm-2023.*-x86_64']} ]}}};
|
|
37
|
+
const checks=[{condition:v('review_approved'),error_message:'Review this concrete component plan, destination and costs before apply.'},
|
|
38
|
+
{condition:'${startswith(var.worker_role_arn,format("arn:aws:iam::%s:role/",var.account_id)) && data.aws_iam_role.'+n+'_worker.arn == var.worker_role_arn}',error_message:'The existing worker must match the full same-account ARN, including its path.'}];
|
|
39
|
+
for(const purpose of ['parser','transfer'])checks.push({condition:'${!var.images_ready || startswith(var.'+purpose+'_image,format("%s.dkr.ecr.%s.amazonaws.com/%s/'+purpose+'@sha256:",var.account_id,var.region,local.name))}',error_message:'Keep '+purpose+' in its owned regional repository.'});
|
|
40
|
+
resource.terraform_data={[n+'_review']:{input:{source_component:literal(row.id),design_revision:project.revision,environment,requested_worker_arn:v('worker_role_arn'),resolved_worker_arn:'${data.aws_iam_role.'+n+'_worker.arn}'},lifecycle:{precondition:checks}}};
|
|
41
|
+
const add=(type,suffix,body)=>((resource[type]??={})[n+suffix]={...body,depends_on:['terraform_data.'+n+'_review',...(body.depends_on||[])]});
|
|
42
|
+
add('aws_ecs_cluster','',{name:l('name'),setting:[{name:'containerInsights',value:'disabled'}],configuration:{execute_command_configuration:{logging:'NONE'}}});
|
|
43
|
+
const module={host_verification:{source:'./modules/host-verification',providers:{aws:'aws'},account_id:v('account_id'),region:v('region'),review_approved:v('review_approved'),cluster_arn:l('cluster_arn'),worker_role_arn:v('worker_role_arn')}};
|
|
44
|
+
add('aws_vpc','',{cidr_block:'10.88.0.0/16',enable_dns_support:true,enable_dns_hostnames:true,tags:{Name:l('name')},lifecycle:{prevent_destroy:true}});
|
|
45
|
+
for(let i=0;i<2;i++){
|
|
46
|
+
add('aws_subnet','_'+i,{vpc_id:r('aws_vpc','','id'),cidr_block:'10.88.'+i+'.0/24',availability_zone:'${var.availability_zones['+i+']}',map_public_ip_on_launch:false});
|
|
47
|
+
add('aws_route_table','_'+i,{vpc_id:r('aws_vpc','','id')});
|
|
48
|
+
add('aws_route_table_association','_'+i,{subnet_id:r('aws_subnet','_'+i,'id'),route_table_id:r('aws_route_table','_'+i,'id')});
|
|
49
|
+
}
|
|
50
|
+
for(const suffix of ['_host','_endpoint'])add('aws_security_group',suffix,{name:l('name')+suffix.replace('_','-'),description:'Private document '+suffix.slice(1)+' boundary; rules managed separately',vpc_id:r('aws_vpc','','id')});
|
|
51
|
+
add('aws_vpc_security_group_ingress_rule','_endpoint',{security_group_id:r('aws_security_group','_endpoint','id'),referenced_security_group_id:r('aws_security_group','_host','id'),ip_protocol:'tcp',from_port:443,to_port:443});
|
|
52
|
+
add('aws_vpc_security_group_egress_rule','_api',{security_group_id:r('aws_security_group','_host','id'),referenced_security_group_id:r('aws_security_group','_endpoint','id'),ip_protocol:'tcp',from_port:443,to_port:443});
|
|
53
|
+
const subnets=[r('aws_subnet','_0','id'),r('aws_subnet','_1','id')];
|
|
54
|
+
// Modern SSM Agent uses ssmmessages; obsolete ec2messages is unavailable in
|
|
55
|
+
// newer regions. No public endpoint fallback exists in this isolated VPC.
|
|
56
|
+
for(const service of ['ecs','ecs-agent','ecs-telemetry','ecr.api','ecr.dkr','ssm','ssmmessages'])add('aws_vpc_endpoint','_'+service.replace(/[.-]/g,'_'),{vpc_id:r('aws_vpc','','id'),service_name:'${format("com.amazonaws.%s.'+service+'",var.region)}',vpc_endpoint_type:'Interface',private_dns_enabled:true,subnet_ids:subnets,security_group_ids:[r('aws_security_group','_endpoint','id')]});
|
|
57
|
+
add('aws_vpc_endpoint','_s3',{vpc_id:r('aws_vpc','','id'),service_name:'${format("com.amazonaws.%s.s3",var.region)}',vpc_endpoint_type:'Gateway',route_table_ids:[r('aws_route_table','_0','id'),r('aws_route_table','_1','id')],
|
|
58
|
+
policy:'${jsonencode({Version="2012-10-17",Statement=[{Effect="Allow",Principal="*",Action=["s3:GetObject","s3:PutObject","s3:GetBucketLocation","s3:GetBucketVersioning"],Resource=[aws_s3_bucket.'+n+'.arn,format("%s/document-jobs/*",aws_s3_bucket.'+n+'.arn),format("arn:aws:s3:::prod-%s-starport-layer-bucket/*",var.region)]}]})}'});
|
|
59
|
+
add('aws_vpc_security_group_egress_rule','_s3',{security_group_id:r('aws_security_group','_host','id'),prefix_list_id:r('aws_vpc_endpoint','_s3','prefix_list_id'),ip_protocol:'tcp',from_port:443,to_port:443});
|
|
60
|
+
add('aws_s3_bucket','',{bucket:'${format("atmem-doc-%s",substr(sha256(format("%s:%s",var.account_id,local.name)),0,20))}',force_destroy:false,lifecycle:{prevent_destroy:true}});
|
|
61
|
+
add('aws_s3_bucket_public_access_block','',{bucket:r('aws_s3_bucket','','id'),block_public_acls:true,block_public_policy:true,ignore_public_acls:true,restrict_public_buckets:true});
|
|
62
|
+
add('aws_s3_bucket_ownership_controls','',{bucket:r('aws_s3_bucket','','id'),rule:{object_ownership:'BucketOwnerEnforced'}});
|
|
63
|
+
add('aws_s3_bucket_server_side_encryption_configuration','',{bucket:r('aws_s3_bucket','','id'),rule:{apply_server_side_encryption_by_default:{sse_algorithm:'AES256'}}});
|
|
64
|
+
// A new bucket has no history; never suspend/adopt an existing versioned bucket.
|
|
65
|
+
add('aws_s3_bucket_versioning','',{bucket:r('aws_s3_bucket','','id'),versioning_configuration:{status:'Disabled'}});
|
|
66
|
+
add('aws_s3_bucket_lifecycle_configuration','',{bucket:r('aws_s3_bucket','','id'),rule:[{id:'orphan-fallback',status:'Enabled',filter:{prefix:'document-jobs/'},expiration:{days:1},abort_incomplete_multipart_upload:{days_after_initiation:1}}]});
|
|
67
|
+
const bucket=r('aws_s3_bucket','','arn'),objects='${format("%s/document-jobs/*",aws_s3_bucket.'+n+'.arn)}';
|
|
68
|
+
locals.bucket_policy={Version:'2012-10-17',Statement:[
|
|
69
|
+
{Sid:'RequireTLS',Effect:'Deny',Principal:'*',Action:'s3:*',Resource:[bucket,'${format("%s/*",aws_s3_bucket.'+n+'.arn)}'],Condition:{Bool:{'aws:SecureTransport':'false'}}},
|
|
70
|
+
{Sid:'RequireExplicitEncryption',Effect:'Deny',Principal:'*',Action:'s3:PutObject',Resource:objects,Condition:{StringNotEquals:{'s3:x-amz-server-side-encryption':'AES256'}}},
|
|
71
|
+
{Sid:'RequireCreateOnly',Effect:'Deny',Principal:'*',Action:'s3:PutObject',Resource:objects,Condition:{StringNotEquals:{'s3:if-none-match':'*'}}}]};
|
|
72
|
+
add('aws_s3_bucket_policy','',{bucket:r('aws_s3_bucket','','id'),policy:'${jsonencode(local.bucket_policy)}',depends_on:['aws_s3_bucket_public_access_block.'+n]});
|
|
73
|
+
for(const purpose of ['parser','transfer'])add('aws_ecr_repository','_'+purpose,{name:l('name')+'/'+purpose,image_tag_mutability:'IMMUTABLE',force_delete:false,encryption_configuration:{encryption_type:'AES256'},image_scanning_configuration:{scan_on_push:true},lifecycle:{prevent_destroy:true}});
|
|
74
|
+
locals.task_trust={Version:'2012-10-17',Statement:[{Effect:'Allow',Principal:{Service:'ecs-tasks.amazonaws.com'},Action:'sts:AssumeRole',Condition:{StringEquals:{'aws:SourceAccount':v('account_id')},ArnLike:{'aws:SourceArn':'${format("arn:aws:ecs:%s:%s:*",var.region,var.account_id)}'}}}]};
|
|
75
|
+
const trust=service=>service==='ecs-tasks.amazonaws.com'?'${jsonencode(local.task_trust)}':JSON.stringify({Version:'2012-10-17',Statement:[{Effect:'Allow',Principal:{Service:service},Action:'sts:AssumeRole'}]});
|
|
76
|
+
for(const purpose of ['host','execution','transfer'])add('aws_iam_role','_'+purpose,{name:l('name')+'-'+purpose,assume_role_policy:trust(purpose==='host'?'ec2.amazonaws.com':'ecs-tasks.amazonaws.com')});
|
|
77
|
+
const policy=suffix=>add('aws_iam_role_policy',suffix,{name:l('name')+suffix.replace('_','-'),role:r('aws_iam_role',suffix,'name'),policy:'${jsonencode(local.'+suffix.slice(1)+'_policy)}'}),allow=(Sid,Action,Resource,Condition)=>({Sid,Effect:'Allow',Action,Resource,...(Condition?{Condition}:{} )});
|
|
78
|
+
locals.transfer_policy={Version:'2012-10-17',Statement:[allow('TemporaryBytesOnly',['s3:GetObject','s3:PutObject'],objects),allow('CheckOwnedStore',['s3:GetBucketLocation','s3:GetBucketVersioning'],bucket)]};policy('_transfer');
|
|
79
|
+
locals.execution_policy={Version:'2012-10-17',Statement:[allow('EcrLogin',['ecr:GetAuthorizationToken'],'*'),allow('OwnedImagesOnly',['ecr:BatchGetImage','ecr:GetDownloadUrlForLayer','ecr:BatchCheckLayerAvailability'],[r('aws_ecr_repository','_parser','arn'),r('aws_ecr_repository','_transfer','arn')])]};policy('_execution');
|
|
80
|
+
const ci='${format("arn:aws:ecs:%s:%s:container-instance/%s/*",var.region,var.account_id,local.name)}',tasks='${format("arn:aws:ecs:%s:%s:task/%s/*",var.region,var.account_id,local.name)}';
|
|
81
|
+
locals.host_policy_document={Version:'2012-10-17',Statement:[allow('RegisterInOwnedCluster',['ecs:RegisterContainerInstance','ecs:DeregisterContainerInstance','ecs:SubmitAttachmentStateChanges','ecs:SubmitContainerStateChange','ecs:SubmitTaskStateChange'],l('cluster_arn')),
|
|
82
|
+
allow('OwnedAgentLifecycle',['ecs:Poll','ecs:StartTelemetrySession','ecs:UpdateContainerInstancesState'],ci,{ArnEquals:{'ecs:cluster':l('cluster_arn')}}),
|
|
83
|
+
allow('DiscoverAgentEndpoint',['ecs:DiscoverPollEndpoint'],'*'),allow('OwnedAgentTags',['ecs:ListTagsForResource'],[ci,tasks]),
|
|
84
|
+
allow('AgentRegistrationTags',['ecs:TagResource'],ci,{StringEquals:{'ecs:CreateAction':'RegisterContainerInstance'}}),
|
|
85
|
+
allow('SsmTransport',['ssm:UpdateInstanceInformation','ssmmessages:CreateControlChannel','ssmmessages:CreateDataChannel','ssmmessages:OpenControlChannel','ssmmessages:OpenDataChannel'],'*'),
|
|
86
|
+
allow('FixedSsmDocument',['ssm:GetDocument','ssm:DescribeDocument'],'${module.host_verification.host_verification.hostDocumentArn}')]};
|
|
87
|
+
add('aws_iam_role_policy','_host',{name:l('name')+'-host',role:r('aws_iam_role','_host','name'),policy:'${jsonencode(local.host_policy_document)}'});
|
|
88
|
+
add('aws_iam_instance_profile','',{name:l('name')+'-host',role:r('aws_iam_role','_host','name')});
|
|
89
|
+
// Resolve only owned static placeholders; no request or client source is evaluated.
|
|
90
|
+
const replacements={'__FAMILY__':l('name')+'-parser','__POLICY_SHA256__':l('host_policy'),'__TRANSFER_ROLE_ARN__':r('aws_iam_role','_transfer','arn'),'__EXECUTION_ROLE_ARN__':r('aws_iam_role','_execution','arn'),'__PARSER_IMAGE__':v('parser_image'),'__TRANSFER_IMAGE__':v('transfer_image')};
|
|
91
|
+
function resolve(value){if(Array.isArray(value))return value.map(resolve);if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,item])=>[key,resolve(item)]));if(typeof value==='string')for(const[key,item]of Object.entries(replacements))value=value.replaceAll(key,item);return value;}
|
|
92
|
+
locals.task_definition=resolve(template);
|
|
93
|
+
// Runtime family is deliberately prefixed document-, independent of display names.
|
|
94
|
+
locals.task_definition.family='${format("document-%s-parser",local.name)}';
|
|
95
|
+
add('aws_ecs_task_definition','',{count:'${var.images_ready ? 1 : 0}',family:'${local.task_definition.family}',network_mode:'bridge',requires_compatibilities:['EC2'],task_role_arn:r('aws_iam_role','_transfer','arn'),execution_role_arn:r('aws_iam_role','_execution','arn'),
|
|
96
|
+
container_definitions:'${jsonencode(local.task_definition.containerDefinitions)}',runtime_platform:{cpu_architecture:'X86_64',operating_system_family:'LINUX'},placement_constraints:[{type:'memberOf',expression:'${local.task_definition.placementConstraints[0].expression}'}],
|
|
97
|
+
volume:['input','output'].map(name=>({name,docker_volume_configuration:{scope:'task',driver:'local',autoprovision:false}})),tags:{'atmem-workload':'document-parser'}});
|
|
98
|
+
locals.worker_policy={Version:'2012-10-17',Statement:[allow('InspectDefinition',['ecs:DescribeTaskDefinition'],'*'),allow('RunPinnedDefinition',['ecs:RunTask'],'${var.images_ready ? aws_ecs_task_definition.'+n+'[0].arn : null}',{ArnEquals:{'ecs:cluster':l('cluster_arn')},Bool:{'ecs:enable-execute-command':'false'}}),
|
|
99
|
+
allow('OwnedTaskLifecycle',['ecs:DescribeTasks','ecs:StopTask'],tasks,{ArnEquals:{'ecs:cluster':l('cluster_arn')}}),allow('DiscoverOwnedRuns',['ecs:ListTasks'],ci,{ArnEquals:{'ecs:cluster':l('cluster_arn')}}),
|
|
100
|
+
allow('PassOnlyTransferAndPull',['iam:PassRole'],[r('aws_iam_role','_transfer','arn'),r('aws_iam_role','_execution','arn')],{StringEquals:{'iam:PassedToService':'ecs-tasks.amazonaws.com'}}),
|
|
101
|
+
allow('OwnedTemporaryBytes',['s3:GetObject','s3:PutObject','s3:DeleteObject'],objects),allow('CheckOwnedStore',['s3:GetBucketLocation','s3:GetBucketVersioning'],bucket)]};
|
|
102
|
+
// Conditional local prevents evaluating [0] while images are not ready.
|
|
103
|
+
locals.enabled_worker_policy='${var.images_ready ? jsonencode(local.worker_policy) : null}';
|
|
104
|
+
add('aws_iam_role_policy','_worker',{count:'${var.images_ready ? 1 : 0}',name:l('name')+'-worker',role:'${data.aws_iam_role.'+n+'_worker.name}',policy:l('enabled_worker_policy')});
|
|
105
|
+
const encode=text=>btoa(Array.from(new TextEncoder().encode(text),byte=>String.fromCharCode(byte)).join(''));
|
|
106
|
+
locals.expected_b64='${base64encode(jsonencode(local.expected))}';
|
|
107
|
+
const guardUnit='[Unit]\nDescription=AtMem private parser host guard\nRequires=docker.service\nAfter=docker.service\nBindsTo=docker.service\nPartOf=docker.service\n\n[Service]\nType=oneshot\nRemainAfterExit=yes\nTimeoutStartSec=60\nExecStart=/usr/bin/python3 -I /opt/atmem/enterprise_aws_host_bootstrap.py guard ${local.expected_b64}\n';
|
|
108
|
+
const ecsDropin='[Unit]\nRequires=atmem-document-guard.service\nAfter=atmem-document-guard.service\nBindsTo=atmem-document-guard.service\nPartOf=docker.service\n';
|
|
109
|
+
locals.cloud_init={package_update:false,package_upgrade:false,bootcmd:[['systemctl','mask','--runtime','--now','ecs.service']],write_files:[
|
|
110
|
+
{path:'/opt/atmem/enterprise_aws_host_probe.py',owner:'root:root',permissions:'0600',encoding:'b64',content:encode(probe)},
|
|
111
|
+
{path:'/opt/atmem/enterprise_aws_host_bootstrap.py',owner:'root:root',permissions:'0600',encoding:'b64',content:encode(bootstrap)},
|
|
112
|
+
{path:'/etc/systemd/system/atmem-document-guard.service',owner:'root:root',permissions:'0644',content:guardUnit},
|
|
113
|
+
{path:'/etc/systemd/system/ecs.service.d/atmem-document.conf',owner:'root:root',permissions:'0644',content:ecsDropin}],
|
|
114
|
+
runcmd:[['/bin/bash','-euo','pipefail','-c','/usr/bin/python3 -I /opt/atmem/enterprise_aws_host_bootstrap.py prepare ${local.expected_b64}\n/usr/bin/systemctl daemon-reload\n/usr/bin/systemctl restart docker.service\n/usr/bin/systemctl start atmem-document-guard.service\n/usr/bin/systemctl unmask --runtime ecs.service\n/usr/bin/systemctl enable --now --no-block ecs.service']]};
|
|
115
|
+
locals.cloud_init_text=['#cloud-config','${jsonencode(local.cloud_init)}'].join(String.fromCharCode(10));
|
|
116
|
+
add('aws_launch_template','',{count:'${var.hosts_enabled ? 1 : 0}',name_prefix:l('name')+'-',image_id:'${data.aws_ami.'+n+'_host[0].id}',instance_type:'m6i.large',update_default_version:false,
|
|
117
|
+
iam_instance_profile:{arn:r('aws_iam_instance_profile','','arn')},network_interfaces:[{associate_public_ip_address:false,security_groups:[r('aws_security_group','_host','id')]}],
|
|
118
|
+
metadata_options:{http_endpoint:'enabled',http_tokens:'required',http_put_response_hop_limit:1,http_protocol_ipv6:'disabled',instance_metadata_tags:'disabled'},
|
|
119
|
+
block_device_mappings:[{device_name:'${data.aws_ami.'+n+'_host[0].root_device_name}',ebs:{encrypted:true,delete_on_termination:true,volume_size:40,volume_type:'gp3'}}],
|
|
120
|
+
user_data:'${base64encode(local.cloud_init_text)}',
|
|
121
|
+
tag_specifications:[{resource_type:'instance',tags:{'atmem-document-policy':l('host_policy'),Name:l('name')}},{resource_type:'volume',tags:{Name:l('name')}}],lifecycle:{create_before_destroy:true}});
|
|
122
|
+
add('aws_autoscaling_group','',{count:'${var.hosts_enabled ? 1 : 0}',name:l('name'),min_size:v('host_count'),max_size:v('host_count'),desired_capacity:v('host_count'),vpc_zone_identifier:subnets,health_check_type:'EC2',
|
|
123
|
+
launch_template:{id:'${aws_launch_template.'+n+'[0].id}',version:'${tostring(aws_launch_template.'+n+'[0].latest_version)}'},
|
|
124
|
+
tag:[{key:'atmem-document-policy',value:l('host_policy'),propagate_at_launch:true}],
|
|
125
|
+
depends_on:['aws_iam_role_policy.'+n+'_host',...Object.keys(resource.aws_vpc_endpoint).map(name=>'aws_vpc_endpoint.'+name),'aws_vpc_security_group_egress_rule.'+n+'_api','aws_vpc_security_group_egress_rule.'+n+'_s3']});
|
|
126
|
+
locals.document_job_configuration={format:'enterprise-aws-document-job-v2',accountId:v('account_id'),region:v('region'),bucket:r('aws_s3_bucket','','id'),clusterArn:l('cluster_arn'),
|
|
127
|
+
hostPolicySha256:l('host_policy'),hostDocumentArn:'${module.host_verification.host_verification.hostDocumentArn}',hostDocumentHash:'${module.host_verification.host_verification.hostDocumentHash}',hostDocumentVersion:'${module.host_verification.host_verification.hostDocumentVersion}',
|
|
128
|
+
workerRoleArn:v('worker_role_arn'),transferRoleArn:r('aws_iam_role','_transfer','arn'),executionRoleArn:r('aws_iam_role','_execution','arn'),parserImage:v('parser_image'),transferImage:v('transfer_image'),taskDefinitionArn:'${var.images_ready ? aws_ecs_task_definition.'+n+'[0].arn : null}'};
|
|
129
|
+
locals.document_executor_configuration={format:'enterprise-document-executor-v1',provider:'aws',parserRevision:v('parser_revision'),configuration:l('document_job_configuration')};
|
|
130
|
+
const output={document_job_configuration:{value:'${var.images_ready && var.hosts_enabled ? local.document_job_configuration : null}',description:'Runtime config only; requires complete worker/export and destination checks.',depends_on:['aws_autoscaling_group.'+n,'aws_iam_role_policy.'+n+'_worker']},
|
|
131
|
+
artifacts:{value:{parserRepository:r('aws_ecr_repository','_parser','repository_url'),transferRepository:r('aws_ecr_repository','_transfer','repository_url'),parserRevision:v('parser_revision'),recommendedAmi:'${nonsensitive(data.aws_ssm_parameter.'+n+'_recommended_ami.value)}',hostAmi:v('host_ami_id')}},
|
|
132
|
+
host_verification:{value:'${module.host_verification.host_verification}'},document_executor_configuration:{value:'${var.images_ready && var.hosts_enabled ? local.document_executor_configuration : null}',description:'Owned worker input envelope, bound to the approved parser revision; full application hosting/export acceptance is separate.'}};
|
|
133
|
+
const main={terraform:child.terraform,provider:{aws:{region:v('region'),allowed_account_ids:[v('account_id')]}},variable,locals,data,module,resource,output};
|
|
134
|
+
return {...enterpriseApplicationFiles('aws',n),...files,'main.tf.json':JSON.stringify(main,null,2)+'\n','.terraform.lock.hcl':host['.terraform.lock.hcl'],'deployment.tfvars.json.example':JSON.stringify(examples,null,2)+'\n',
|
|
135
|
+
'tests/document-infrastructure.tftest.hcl':awsDocumentInfrastructureTests(n,seed,row.region),
|
|
136
|
+
'document-job-boundary.json':JSON.stringify({provider:'aws',region:row.region,component:row.id,scope:'Private parser execution substrate; complete application/export is unfinished.',cloudOperations:false,wholeProjectQualified:false},null,2)+'\n',
|
|
137
|
+
'README.md':'# AWS private parser substrate\n\nCreates a new dedicated private VPC, two subnets, API/ECR/SSM endpoints, temporary no-history S3 store, separate host/transfer/image-pull roles, immutable repositories, fixed host verifier and optional EC2 pool/static task revision. Existing worker identity is referenced by its full ARN and receives additive named policies. No existing pool, database, document source or website is adopted.\n\nReview costs before foundation apply: endpoints incur standing charges even with hosts disabled. Hosts are a fixed bounded standing pool, not automatically scaled or performance-qualified. Supply actual reviewed AMI and independently qualified immutable images; discovery metadata does not approve them. Runtime output remains null until both switches are enabled.\n\nNo NAT, internet gateway, public IP or SSH ingress is created. Trusted transfer stages can reach the named private APIs and the S3 gateway prefix list; the parser has networking disabled. Physical erasure, native host/agent/SSM/IAM behavior, strict retention and throughput require destination qualification. One-day lifecycle is an asynchronous fallback, not a deadline.\n\nTask definitions belong to Terraform, not the worker. Upgrade by publishing a new reviewed revision and draining active work before replacing hosts; automatic rolling host replacement/draining is not implemented. Keep the old cleanup target available when changing family/bucket/destination. Never destroy populated stores or terminate active hosts without a reviewed drain.\n\nComplete API/worker/SQL/model/source hosting, enrollment, secrets, recovery and Spec Kit/export composition remain unfinished. No whole-project, security, cloud-plan or deployment acceptance is implied.\n'};
|
|
138
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {resourceAddress} from './identifiers.mjs';
|
|
2
|
+
import {CLOUD_PROVIDER_LOCKS} from './cloud-provider-locks.mjs';
|
|
3
|
+
import {googleDocumentJobTests} from './enterprise-google-tests.mjs';
|
|
4
|
+
import {enterpriseApplicationFiles}from'./enterprise-application.mjs';
|
|
5
|
+
|
|
6
|
+
// Partial compiler building block. Do not register a complete private-assistant
|
|
7
|
+
// capability until application/operator/recovery and the other clouds qualify.
|
|
8
|
+
export function googleDocumentJobTerraform(project,row,environment='staging'){
|
|
9
|
+
if(!project||typeof project.id!=='string'||!project.id||project.id.length>255||!Number.isSafeInteger(project.revision)||project.revision<1)throw Error('Use the stable owned project and design revision');
|
|
10
|
+
if(!row||row.provider!=='gcp'||typeof row.id!=='string'||!row.id||typeof row.address!=='string'||!/^block_[0-9a-f]+$/.test(row.address)||typeof row.region!=='string'||!(/^[a-z]+-[a-z]+[1-9][0-9]?$/).test(row.region))throw Error('Preserve the explicit Google provider and canonical selected region');
|
|
11
|
+
encodeURIComponent(row.id); // Reject invalid Unicode instead of replacing bytes.
|
|
12
|
+
const address=resourceAddress(row.id);
|
|
13
|
+
if(row.address!==address)throw Error('Keep the canonical source-component address; do not redirect another component');
|
|
14
|
+
if(!['staging','production','qualification'].includes(environment))throw Error('Use an explicit supported environment');
|
|
15
|
+
const n=row.address,seed='https://atmem.ai/workspaces/'+encodeURIComponent(project.id)+'/'+n+'/'+environment;
|
|
16
|
+
const v=key=>'${var.'+key+'}',r=(type,key,attribute)=>'${'+type+'.'+key+'.'+attribute+'}',l=key=>'${local.'+key+'}';
|
|
17
|
+
const variable={review_approved:{type:'bool',default:false,nullable:false,description:'Review the exact destination, IAM scope, raw-data retention, costs and operating responsibilities before any apply.'},images_ready:{type:'bool',default:false,nullable:false,description:'Enable configuration output only after actual immutable parser/transfer artifacts are built and reviewed.'}};
|
|
18
|
+
const examples={review_approved:false,images_ready:false},resource={},output={};
|
|
19
|
+
function input(name,description,example,pattern,defaultValue){
|
|
20
|
+
variable[name]={type:'string',nullable:false,description,validation:{condition:'${can(regex('+JSON.stringify(pattern)+',var.'+name+'))}',error_message:'Supply the reviewed '+name+' destination input.'}};
|
|
21
|
+
if(defaultValue!==undefined)variable[name].default=defaultValue;examples[name]=example;return v(name);
|
|
22
|
+
}
|
|
23
|
+
const destination=input('project_id','Existing owned project in which this package administers Batch jobs. Review project-scoped control permissions; unrelated workloads are not adopted.','reviewed-project','^[a-z][a-z0-9-]{4,28}[a-z0-9]$');
|
|
24
|
+
const region=input('region','Placement is bound to the selected architecture; regenerate to change it.',row.region,'^'+row.region+'$',row.region);
|
|
25
|
+
const worker=input('worker_service_account','Existing question-worker service account in this project, referenced without changing its other roles or SQL identity.','owned-worker@reviewed-project.iam.gserviceaccount.com','^[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9][.]iam[.]gserviceaccount[.]com$');
|
|
26
|
+
const locals={name:'${format("doc-%s",substr(sha256('+JSON.stringify(seed)+'),0,12))}'};
|
|
27
|
+
const checks=[{condition:v('review_approved'),error_message:'Review the destination and complete this component plan before any apply.'},
|
|
28
|
+
{condition:'${endswith(var.worker_service_account,format("@%s.iam.gserviceaccount.com",var.project_id))}',error_message:'The existing worker identity must belong to this destination project.'},
|
|
29
|
+
{condition:'${var.worker_service_account != format("%s-vm@%s.iam.gserviceaccount.com",local.name,var.project_id)}',error_message:'Keep question-worker and parser-transfer VM identities separate.'}];
|
|
30
|
+
for(const purpose of ['parser','transfer']){
|
|
31
|
+
const name=purpose+'_image';
|
|
32
|
+
variable[name]={type:'string',default:'',nullable:false,description:'Immutable package-owned regional '+purpose+' artifact; empty only during foundation.',validation:{condition:'${!var.images_ready || can(regex("^[a-z0-9./_-]+@sha256:[0-9a-f]{64}$",var.'+name+'))}',error_message:'Use the actual reviewed immutable '+purpose+' artifact.'}};
|
|
33
|
+
examples[name]='';
|
|
34
|
+
checks.push({condition:'${!var.images_ready || startswith(var.'+name+',format("%s-docker.pkg.dev/%s/%s/'+purpose+'@sha256:",var.region,var.project_id,local.name))}',error_message:'Keep '+purpose+' in the owned regional registry with a reviewed digest.'});
|
|
35
|
+
}
|
|
36
|
+
variable.parser_revision={type:'string',default:'',nullable:false,description:'Full independently qualified parser artifact revision, supplied by the image build receipt, not guessed from a source revision.',validation:{condition:'${!var.images_ready || can(regex("^[0-9a-f]{64}$",var.parser_revision))}',error_message:'Supply the actual qualified parser artifact revision.'}};examples.parser_revision='';
|
|
37
|
+
const literal=value=>value.replace(/\$\{/g,()=>'$${').replace(/%\{/g,()=>'%%{');
|
|
38
|
+
resource.terraform_data={[n+'_review']:{input:{design_revision:project.revision,environment,source_component:literal(row.id)},lifecycle:{precondition:checks}}};
|
|
39
|
+
const add=(type,name,body)=>((resource[type]??={})[name]={...body,depends_on:['terraform_data.'+n+'_review',...(body.depends_on||[])]});
|
|
40
|
+
const apiDependencies=[];
|
|
41
|
+
for(const [key,service]of Object.entries({compute:'compute.googleapis.com',batch:'batch.googleapis.com',identity:'iam.googleapis.com',storage:'storage.googleapis.com',registry:'artifactregistry.googleapis.com',dns:'dns.googleapis.com'})){
|
|
42
|
+
add('google_project_service',n+'_'+key,{project:destination,service,disable_on_destroy:false});apiDependencies.push('google_project_service.'+n+'_'+key);
|
|
43
|
+
}
|
|
44
|
+
add('google_project_service_identity',n+'_batch_agent',{provider:'google-beta',project:destination,service:'batch.googleapis.com',depends_on:['google_project_service.'+n+'_batch']});
|
|
45
|
+
add('google_project_iam_member',n+'_batch_agent',{project:destination,role:'roles/batch.serviceAgent',member:r('google_project_service_identity',n+'_batch_agent','member')});
|
|
46
|
+
add('google_service_account',n+'_vm',{project:destination,account_id:l('name')+'-vm',display_name:'Trusted one-file transfer and Batch reporting; no source/model/SQL role',depends_on:['google_project_service.'+n+'_identity']});
|
|
47
|
+
const vmMember='${format("serviceAccount:%s",google_service_account.'+n+'_vm.email)}',workerMember='${format("serviceAccount:%s",var.worker_service_account)}';
|
|
48
|
+
add('google_project_iam_member',n+'_reporter',{project:destination,role:'roles/batch.agentReporter',member:vmMember,depends_on:['google_project_service.'+n+'_batch']});
|
|
49
|
+
add('google_service_account_iam_member',n+'_worker_act_as',{service_account_id:r('google_service_account',n+'_vm','name'),role:'roles/iam.serviceAccountUser',member:workerMember});
|
|
50
|
+
add('google_project_iam_custom_role',n+'_control',{project:destination,role_id:'${replace(local.name,"-","_")}_control',title:'Owned private document job controller',stage:'GA',permissions:['batch.jobs.create','batch.jobs.get','batch.jobs.delete','batch.operations.get'],depends_on:['google_project_service.'+n+'_batch']});
|
|
51
|
+
add('google_project_iam_member',n+'_worker_control',{project:destination,role:r('google_project_iam_custom_role',n+'_control','name'),member:workerMember});
|
|
52
|
+
add('google_artifact_registry_repository',n,{project:destination,location:region,repository_id:l('name'),format:'DOCKER',description:'Reviewed immutable parser and trusted transfer artifacts',depends_on:['google_project_service.'+n+'_registry'],lifecycle:{prevent_destroy:true}});
|
|
53
|
+
add('google_artifact_registry_repository_iam_member',n+'_image_read',{project:destination,location:region,repository:r('google_artifact_registry_repository',n,'repository_id'),role:'roles/artifactregistry.reader',member:vmMember});
|
|
54
|
+
add('google_storage_bucket',n,{project:destination,name:'${format("atmem-doc-%s",substr(sha256(format("%s:%s",var.project_id,local.name)),0,20))}',location:region,storage_class:'STANDARD',uniform_bucket_level_access:true,public_access_prevention:'enforced',force_destroy:false,
|
|
55
|
+
versioning:{enabled:false},soft_delete_policy:{retention_duration_seconds:0},lifecycle_rule:[{action:{type:'Delete'},condition:{age:1,matches_prefix:['document-jobs/']}}],lifecycle:{prevent_destroy:true},depends_on:['google_project_service.'+n+'_storage']});
|
|
56
|
+
for(const [role,permissions,member]of [['transfer',['storage.objects.get','storage.objects.create'],vmMember],['worker',['storage.objects.get','storage.objects.create','storage.objects.delete'],workerMember]]){
|
|
57
|
+
add('google_project_iam_custom_role',n+'_'+role+'_objects',{project:destination,role_id:'${replace(local.name,"-","_")}_'+role+'_objects',title:'Private parser '+role+' temporary bytes',stage:'GA',permissions,depends_on:['google_project_service.'+n+'_storage']});
|
|
58
|
+
add('google_storage_bucket_iam_member',n+'_'+role,{bucket:r('google_storage_bucket',n,'name'),role:r('google_project_iam_custom_role',n+'_'+role+'_objects','name'),member});
|
|
59
|
+
}
|
|
60
|
+
add('google_compute_network',n,{project:destination,name:l('name'),auto_create_subnetworks:false,delete_default_routes_on_create:true,depends_on:['google_project_service.'+n+'_compute'],lifecycle:{prevent_destroy:true}});
|
|
61
|
+
add('google_compute_subnetwork',n,{project:destination,name:l('name'),region,network:r('google_compute_network',n,'id'),ip_cidr_range:'10.87.0.0/24',private_ip_google_access:true,lifecycle:{prevent_destroy:true}});
|
|
62
|
+
// No NAT/public IP or broad internet route: only Google private API VIPs and
|
|
63
|
+
// the documented direct connectivity range are reachable from trusted VMs.
|
|
64
|
+
for(const [suffix,cidr]of [['api','199.36.153.8/30'],['direct','34.126.0.0/18']])add('google_compute_route',n+'_'+suffix,{project:destination,name:l('name')+'-'+suffix,network:r('google_compute_network',n,'id'),dest_range:cidr,next_hop_gateway:'default-internet-gateway',priority:1000});
|
|
65
|
+
add('google_compute_firewall',n+'_allow_api',{project:destination,name:l('name')+'-api',network:r('google_compute_network',n,'id'),direction:'EGRESS',priority:1000,destination_ranges:['199.36.153.8/30','34.126.0.0/18'],target_service_accounts:[r('google_service_account',n+'_vm','email')],allow:[{protocol:'tcp',ports:['443']}]});
|
|
66
|
+
add('google_compute_firewall',n+'_deny_egress',{project:destination,name:l('name')+'-deny-egress',network:r('google_compute_network',n,'id'),direction:'EGRESS',priority:2000,destination_ranges:['0.0.0.0/0'],deny:[{protocol:'all'}]});
|
|
67
|
+
add('google_compute_firewall',n+'_deny_ingress',{project:destination,name:l('name')+'-deny-ingress',network:r('google_compute_network',n,'id'),direction:'INGRESS',priority:1000,source_ranges:['0.0.0.0/0'],deny:[{protocol:'all'}]});
|
|
68
|
+
for(const [key,domain]of [['apis','googleapis.com.'],['registry','pkg.dev.']]){
|
|
69
|
+
add('google_dns_managed_zone',n+'_'+key,{project:destination,name:l('name')+'-'+key,dns_name:domain,visibility:'private',description:'Owned private parser API resolution',private_visibility_config:{networks:[{network_url:r('google_compute_network',n,'id')}]},depends_on:['google_project_service.'+n+'_dns'],lifecycle:{prevent_destroy:true}});
|
|
70
|
+
add('google_dns_record_set',n+'_'+key+'_wildcard',{project:destination,managed_zone:r('google_dns_managed_zone',n+'_'+key,'name'),name:'*.'+domain,type:'CNAME',ttl:300,rrdatas:['private.googleapis.com.']});
|
|
71
|
+
}
|
|
72
|
+
add('google_dns_record_set',n+'_private_vip',{project:destination,managed_zone:r('google_dns_managed_zone',n+'_apis','name'),name:'private.googleapis.com.',type:'A',ttl:300,rrdatas:['199.36.153.8','199.36.153.9','199.36.153.10','199.36.153.11']});
|
|
73
|
+
locals.document_job_configuration={format:'enterprise-google-document-job-v1',projectId:destination,region,bucket:r('google_storage_bucket',n,'name'),parserImage:v('parser_image'),transferImage:v('transfer_image'),workerServiceAccount:worker,vmServiceAccount:r('google_service_account',n+'_vm','email'),network:'${format("projects/%s/global/networks/%s",var.project_id,google_compute_network.'+n+'.name)}',subnetwork:'${format("projects/%s/regions/%s/subnetworks/%s",var.project_id,var.region,google_compute_subnetwork.'+n+'.name)}'};
|
|
74
|
+
output.document_job_configuration={value:'${var.images_ready ? local.document_job_configuration : null}',description:'Exact runtime adapter shape; not evidence of worker wiring, crash recovery or account acceptance.',depends_on:Object.entries(resource).flatMap(([type,items])=>Object.keys(items).map(name=>type+'.'+name))};
|
|
75
|
+
locals.document_executor_configuration={format:'enterprise-document-executor-v1',provider:'gcp',parserRevision:v('parser_revision'),configuration:l('document_job_configuration')};
|
|
76
|
+
output.document_executor_configuration={value:'${var.images_ready ? local.document_executor_configuration : null}',description:'Owned worker envelope matching the approved parser revision; complete application hosting/export acceptance remains separate.'};
|
|
77
|
+
output.document_job_artifacts={value:{repository:r('google_artifact_registry_repository',n,'repository_id'),parserRevision:v('parser_revision'),parserImage:v('parser_image'),transferImage:v('transfer_image')}};
|
|
78
|
+
const main={terraform:{required_version:'>= 1.17.1, < 2.0.0',required_providers:{google:{source:'hashicorp/google',version:'7.46.2'},'google-beta':{source:'hashicorp/google-beta',version:'7.46.2'}}},provider:{google:{project:destination,region},'google-beta':{project:destination,region}},variable,locals,resource,output};
|
|
79
|
+
return{...enterpriseApplicationFiles('gcp',n),'main.tf.json':JSON.stringify(main,null,2)+'\n','.terraform.lock.hcl':CLOUD_PROVIDER_LOCKS.gcp+'\n'+CLOUD_PROVIDER_LOCKS.gcpBeta,
|
|
80
|
+
'deployment.tfvars.json.example':JSON.stringify(examples,null,2)+'\n',
|
|
81
|
+
'tests/document-jobs.tftest.hcl':googleDocumentJobTests(n,seed,row.region),
|
|
82
|
+
'document-job-boundary.json':JSON.stringify({provider:'gcp',component:row.id,address:n,environment,region:row.region,designRevision:project.revision,scope:'Temporary parser substrate only; not the complete private assistant.',existingWorker:'Referenced, not recreated. Other existing IAM memberships are not changed.',crashRecovery:'Unfinished. Lifecycle age=1 is an asynchronous fallback request, not a maximum retention guarantee.',productionQualified:false,cloudOperations:false},null,2)+'\n',
|
|
83
|
+
'README.md':'# Private Google document substrate\n\nGenerated from the owned design; this is one unfinished composition block, not a complete project. Use the same selected region, owned destination and existing question-worker identity. Terraform manages only the declared bucket, parser VM identity, custom roles, private registry/network/DNS and API prerequisites. It does not create a user website, company identity provider or document sources.\n\nThe controller gets four project-scoped Batch permissions and actAs only on the transfer VM identity. Object permissions are separate custom roles bound solely to the ephemeral bucket; the transfer VM cannot delete objects or access source/model/SQL credentials through these grants. Existing inherited permissions still require account inspection.\n\nBuild/review immutable artifacts after the foundation and set images_ready only with real build receipts. Foundation output is null; image inputs contain no synthetic published digest. Generated runtime configuration matches enterprise_google_jobs.py. Application hosting, provider/artifact binding, durable orphan recovery, key/audit operations and complete Spec Kit/export integration remain unfinished. No private capability or whole-case acceptance is implied.\n\nOrdinary completion deletes temporary input/results. Bucket versioning and soft deletion are disabled to avoid hidden retained copies. No retention lock or replication is requested. Age-one-day lifecycle is an asynchronous last-resort cleanup request; it does not provide a strict upper retention bound or replace the required reconciler. Observe organization policies and deletion propagation in the destination before release.\n\nVMs have no external IP in the emitted runtime request. Private Google Access, private googleapis.com/pkg.dev DNS, explicit private API routes, TCP443 allow rules and deny-all egress/ingress supply the substrate. Batch-container network isolation and actual VM/agent/IAM/DNS behavior require destination testing; mock plans do not prove those behaviors. No cloud apply or deployment was performed.\n'};
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Credential-free provider mock plans are shipped with this partial module.
|
|
2
|
+
// They verify emitted composition, never account IAM/network enforcement.
|
|
3
|
+
export function googleDocumentJobTests(n,seed,region){
|
|
4
|
+
const digest='a'.repeat(64),name=`format("doc-%s",substr(sha256(${JSON.stringify(seed)}),0,12))`;
|
|
5
|
+
const image=purpose=>`format("%s-docker.pkg.dev/reviewed-project/%s/${purpose}@sha256:${digest}",${JSON.stringify(region)},${name})`;
|
|
6
|
+
const run=(title,body)=>`run "${title}" {\n command = plan\n${body}\n}\n`;
|
|
7
|
+
const assertion=(condition,message)=>` assert {\n condition = ${condition}\n error_message = ${JSON.stringify(message)}\n }`;
|
|
8
|
+
const ref=(type,suffix='')=>type+'.'+n+suffix;
|
|
9
|
+
let text=`mock_provider "google" {}\nmock_provider "google-beta" {}\n`;
|
|
10
|
+
text+=`override_resource {\n override_during = plan\n target = ${ref('google_service_account','_vm')}\n values = {\n email = "document-vm@reviewed-project.iam.gserviceaccount.com"\n name = "projects/reviewed-project/serviceAccounts/document-vm@reviewed-project.iam.gserviceaccount.com"\n }\n}\n`;
|
|
11
|
+
text+=`override_resource {\n override_during = plan\n target = ${ref('google_project_service_identity','_batch_agent')}\n values = {\n email = "service-123456789012@gcp-sa-cloudbatch.iam.gserviceaccount.com"\n member = "serviceAccount:service-123456789012@gcp-sa-cloudbatch.iam.gserviceaccount.com"\n }\n}\n`;
|
|
12
|
+
text+=`variables {\n review_approved = true\n images_ready = true\n project_id = "reviewed-project"\n worker_service_account = "owned-worker@reviewed-project.iam.gserviceaccount.com"\n parser_image = ${image('parser')}\n transfer_image = ${image('transfer')}\n parser_revision = "${digest}"\n}\n`;
|
|
13
|
+
text+=run('private_document_substrate',assertion(`${ref('google_storage_bucket')}.uniform_bucket_level_access && ${ref('google_storage_bucket')}.public_access_prevention == "enforced" && !${ref('google_storage_bucket')}.versioning[0].enabled && ${ref('google_storage_bucket')}.soft_delete_policy[0].retention_duration_seconds == 0 && !${ref('google_storage_bucket')}.force_destroy`,'No public access, mutable history or hidden soft-delete retention for temporary document bytes.')+'\n'+assertion(`output.document_executor_configuration.provider == "gcp" && output.document_executor_configuration.parserRevision == var.parser_revision && output.document_executor_configuration.configuration == output.document_job_configuration && output.document_job_configuration.format == "enterprise-google-document-job-v1" && output.document_job_configuration.region == ${JSON.stringify(region)} && output.document_job_configuration.workerServiceAccount == var.worker_service_account && output.document_job_configuration.vmServiceAccount == ${ref('google_service_account','_vm')}.email`,'Keep exact runtime configuration, selected region and separate identities.'));
|
|
14
|
+
text+=run('scoped_job_identity',assertion(`toset(${ref('google_project_iam_custom_role','_control')}.permissions) == toset(["batch.jobs.create","batch.jobs.get","batch.jobs.delete","batch.operations.get"]) && ${ref('google_service_account_iam_member','_worker_act_as')}.service_account_id == ${ref('google_service_account','_vm')}.name && ${ref('google_service_account_iam_member','_worker_act_as')}.role == "roles/iam.serviceAccountUser"`,'Do not grant generic administrator or project-wide actAs authority.')+'\n'+assertion(`toset(${ref('google_project_iam_custom_role','_transfer_objects')}.permissions) == toset(["storage.objects.get","storage.objects.create"]) && toset(${ref('google_project_iam_custom_role','_worker_objects')}.permissions) == toset(["storage.objects.get","storage.objects.create","storage.objects.delete"]) && ${ref('google_storage_bucket_iam_member','_transfer')}.bucket == ${ref('google_storage_bucket')}.name`,'Transfer identities receive temporary-byte access only; deletion stays with the controller.'));
|
|
15
|
+
text+=run('private_network',assertion(`${ref('google_compute_subnetwork')}.private_ip_google_access && !${ref('google_compute_network')}.auto_create_subnetworks && ${ref('google_compute_network')}.delete_default_routes_on_create && ${ref('google_compute_route','_api')}.dest_range == "199.36.153.8/30" && ${ref('google_compute_route','_direct')}.dest_range == "34.126.0.0/18"`,'Retain explicit private Google routing without a default internet route.')+'\n'+assertion(`${ref('google_compute_firewall','_allow_api')}.priority < ${ref('google_compute_firewall','_deny_egress')}.priority && ${ref('google_compute_firewall','_deny_egress')}.direction == "EGRESS" && ${ref('google_dns_record_set','_private_vip')}.type == "A" && toset(${ref('google_dns_record_set','_registry_wildcard')}.rrdatas) == toset(["private.googleapis.com."])`,'Private API/registry DNS and restrictive firewall ordering must remain connected.'));
|
|
16
|
+
text+=run('foundation_configuration_not_ready',` variables {\n images_ready = false\n parser_image = ""\n transfer_image = ""\n parser_revision = ""\n }\n`+assertion('output.document_job_configuration == null','Do not advertise a configured parser before real artifacts are published.'));
|
|
17
|
+
text+=run('review_required',` variables { review_approved = false }\n expect_failures = [terraform_data.${n}_review]`);
|
|
18
|
+
text+=run('foreign_worker_rejected',` variables { worker_service_account = "owned-worker@foreign-project.iam.gserviceaccount.com" }\n expect_failures = [terraform_data.${n}_review]`);
|
|
19
|
+
text+=run('same_worker_and_vm_rejected',` variables { worker_service_account = format("%s-vm@reviewed-project.iam.gserviceaccount.com",${name}) }\n expect_failures = [terraform_data.${n}_review]`);
|
|
20
|
+
text+=run('mutable_image_rejected',` variables { parser_image = "unreviewed:latest" }\n expect_failures = [var.parser_image]`);
|
|
21
|
+
text+=run('foreign_registry_rejected',` variables { transfer_image = "foreign.example/transfer@sha256:${digest}" }\n expect_failures = [terraform_data.${n}_review]`);
|
|
22
|
+
text+=run('missing_parser_receipt_rejected',` variables { parser_revision = "" }\n expect_failures = [var.parser_revision]`);
|
|
23
|
+
text+=run('selected_region_cannot_drift',` variables { region = "us-central1" }\n expect_failures = [var.region]`);
|
|
24
|
+
return text;
|
|
25
|
+
}
|
package/src/estimate.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Monthly run-cost estimate. See specs/004-cost-estimate.
|
|
2
|
+
// Every line is quantity (from usage inputs) × unit price (entered with a source).
|
|
3
|
+
// No prices ship with AtMem AI Architect; a missing input or price leaves the estimate incomplete.
|
|
4
|
+
import { roleOf } from "./review.mjs";
|
|
5
|
+
|
|
6
|
+
export const DISCLAIMER = "Estimate — not a bill or guarantee.";
|
|
7
|
+
|
|
8
|
+
export const INPUTS = [
|
|
9
|
+
{ id: "requests_per_month", label: "Requests per month", unit: "requests", fact: "requests.per_month" },
|
|
10
|
+
{ id: "tokens_per_request", label: "Tokens per request (in + out)", unit: "tokens", hint: "A short question and answer with a few retrieved passages is often 2,000–4,000 tokens." },
|
|
11
|
+
{ id: "data_gb", label: "Data stored", unit: "GB", fact: "data.gb" },
|
|
12
|
+
{ id: "endpoint_hours", label: "Model endpoint hours per month", unit: "hours", assumption: 730, assumptionText: "always on (730 h/month)" },
|
|
13
|
+
{ id: "events_per_month", label: "Events or messages per month", unit: "events" },
|
|
14
|
+
{ id: "training_hours", label: "Training hours per month", unit: "hours" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Planning assumptions, not provider prices or capacity guarantees. Explicit inputs
|
|
18
|
+
// and discovered business facts always take precedence over these presets.
|
|
19
|
+
export const PROJECT_SIZES = Object.freeze([
|
|
20
|
+
{id:'small', label:'Small', requests_per_month:10000, data_gb:10, events_per_month:10000},
|
|
21
|
+
{id:'medium', label:'Medium', requests_per_month:100000, data_gb:100, events_per_month:100000},
|
|
22
|
+
{id:'large', label:'Large', requests_per_month:1000000, data_gb:1000, events_per_month:1000000},
|
|
23
|
+
{id:'extra-large', label:'Extra Large', requests_per_month:10000000, data_gb:10000, events_per_month:10000000},
|
|
24
|
+
].map(size=>Object.freeze({...size,tokens_per_request:3000,endpoint_hours:730})));
|
|
25
|
+
export function setProjectSize(project, id) {
|
|
26
|
+
if (!PROJECT_SIZES.some(size=>size.id===id)) throw Error('Choose a listed project size.');
|
|
27
|
+
return {...project,estimate:{...project.estimate,projectSize:id}};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// role/kind → how it is charged
|
|
31
|
+
const DRIVERS = {
|
|
32
|
+
llm: { name: "Model tokens", unit: "1k tokens", inputs: ["requests_per_month", "tokens_per_request"], qty: (u) => (u.requests_per_month * u.tokens_per_request) / 1000 },
|
|
33
|
+
index: { name: "Index storage", unit: "GB-month", inputs: ["data_gb"], qty: (u) => u.data_gb },
|
|
34
|
+
ingest: { name: "Data processed", unit: "GB", inputs: ["data_gb"], qty: (u) => u.data_gb },
|
|
35
|
+
data: { name: "Storage", unit: "GB-month", inputs: ["data_gb"], qty: (u) => u.data_gb },
|
|
36
|
+
model: { name: "Endpoint hours", unit: "hours", inputs: ["endpoint_hours"], qty: (u) => u.endpoint_hours },
|
|
37
|
+
guard: { name: "Checked requests", unit: "1k requests", inputs: ["requests_per_month"], qty: (u) => u.requests_per_month / 1000 },
|
|
38
|
+
stream: { name: "Messages", unit: "million events", inputs: ["events_per_month"], qty: (u) => u.events_per_month / 1e6 },
|
|
39
|
+
train: { name: "Training hours", unit: "hours", inputs: ["training_hours"], qty: (u) => u.training_hours },
|
|
40
|
+
};
|
|
41
|
+
function driverOf(n) {
|
|
42
|
+
if (n.data.kind === "stream") return DRIVERS.stream;
|
|
43
|
+
if (n.data.kind === "train") return DRIVERS.train;
|
|
44
|
+
return DRIVERS[roleOf(n)] || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Usage values with where they came from: entered, discovery, or a stated assumption.
|
|
48
|
+
export function usage(project) {
|
|
49
|
+
const est = project.estimate || {};
|
|
50
|
+
const facts = project.facts || [];
|
|
51
|
+
return Object.fromEntries(
|
|
52
|
+
INPUTS.map((i) => {
|
|
53
|
+
const entered = est.usage?.[i.id];
|
|
54
|
+
const fromFact = i.fact && [...facts].reverse().find((f) => f.name === i.fact);
|
|
55
|
+
if (Number.isFinite(entered) && entered >= 0) return [i.id, { value: entered, source: "entered" }];
|
|
56
|
+
if (fromFact && Number.isFinite(fromFact.value) && fromFact.value >= 0) return [i.id, { value: fromFact.value, source: `discovery (${fromFact.madeAt})` }];
|
|
57
|
+
const preset = PROJECT_SIZES.find(size=>size.id===est.projectSize);
|
|
58
|
+
if (preset && Number.isFinite(preset[i.id])) return [i.id, {value:preset[i.id], source:`${preset.label} planning assumption`}];
|
|
59
|
+
if (i.assumption != null) return [i.id, { value: i.assumption, source: `assumption: ${i.assumptionText}` }];
|
|
60
|
+
return [i.id, { value: null, source: "needed" }];
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function estimate(project, now = new Date()) {
|
|
66
|
+
const est = project.estimate || {};
|
|
67
|
+
const u = usage(project);
|
|
68
|
+
const values = Object.fromEntries(Object.entries(u).map(([k, v]) => [k, v.value]));
|
|
69
|
+
const low = Number.isFinite(est.low) && est.low >= 0 && est.low <= 1 ? est.low : 0.7;
|
|
70
|
+
const high = Number.isFinite(est.high) && est.high >= 1 ? est.high : 1.5;
|
|
71
|
+
const lines = [];
|
|
72
|
+
const notCosted = [];
|
|
73
|
+
for (const n of project.nodes.filter((x) => x.data.kind !== "note")) {
|
|
74
|
+
const detail = (est.details || []).filter((l) => l.componentId === n.id);
|
|
75
|
+
if (detail.length) {
|
|
76
|
+
for (const l of detail) {
|
|
77
|
+
const validNumber = (v) => Number.isFinite(v) && v >= 0;
|
|
78
|
+
const missing = validNumber(l.quantity) ? [] : ["quantity"];
|
|
79
|
+
const priced = validNumber(l.price) && /^[A-Z]{3}$/.test(l.currency || "") && l.source?.trim() && l.unit?.trim();
|
|
80
|
+
lines.push({ id: l.id, component: n.data.label, componentId: n.id, detailed: true,
|
|
81
|
+
driver: l.description || "Charge", unit: l.unit, inputs: [], quantity: missing.length ? null : l.quantity,
|
|
82
|
+
missing, price: priced ? l : null,
|
|
83
|
+
cost: !missing.length && priced ? Math.round(l.quantity * l.price * 100) / 100 : null,
|
|
84
|
+
status: missing.length ? "input needed" : priced ? "priced" : "price needed" });
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const d = driverOf(n);
|
|
89
|
+
if (!d) {
|
|
90
|
+
notCosted.push(n.data.label);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const missing = d.inputs.filter((k) => values[k] == null);
|
|
94
|
+
const quantity = missing.length ? null : Math.round(d.qty(values) * 100) / 100;
|
|
95
|
+
const price = est.prices?.[n.id];
|
|
96
|
+
const priced = price && Number.isFinite(price.price) && price.price >= 0 && /^[A-Z]{3}$/.test(price.currency || "") && String(price.source || "").trim() && !(price.tierLimit && quantity > price.tierLimit);
|
|
97
|
+
lines.push({
|
|
98
|
+
id: n.id,
|
|
99
|
+
component: n.data.label,
|
|
100
|
+
driver: d.name,
|
|
101
|
+
unit: d.unit,
|
|
102
|
+
inputs: d.inputs,
|
|
103
|
+
quantity,
|
|
104
|
+
missing,
|
|
105
|
+
price: priced ? price : null,
|
|
106
|
+
cost: quantity != null && priced ? Math.round(quantity * price.price * 100) / 100 : null,
|
|
107
|
+
status: missing.length ? "input needed" : priced ? "priced" : "price needed",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const costed = lines.filter((l) => l.cost != null);
|
|
111
|
+
const currencies = [...new Set(costed.map((l) => l.price.currency || "?"))];
|
|
112
|
+
const total = costed.reduce((sum, l) => sum + l.cost, 0);
|
|
113
|
+
const budget = [...(project.facts || [])].reverse().find((f) => f.name === "budget.per_month");
|
|
114
|
+
const budgetCurrency = budget?.unit?.split("/")[0];
|
|
115
|
+
const gaps = [
|
|
116
|
+
...(est.details || []).filter((l) => !project.nodes.some((n) => n.id === l.componentId && n.data.kind !== "note")).map((l) => `${l.description || l.id}: linked component was removed; reassign this charge (excluded from totals).`),
|
|
117
|
+
...lines.filter((l) => l.status !== "priced").map((l) => `${l.component} / ${l.driver}: ${l.status}${l.missing.length ? ` (${l.missing.join(", ")})` : ""}.`),
|
|
118
|
+
...notCosted.map((name) => `${name}: no cost lines; add charges or an explicit zero-cost line with a reason.`),
|
|
119
|
+
...(currencies.length > 1 ? ["Choose one currency; different currencies cannot be added."] : []),
|
|
120
|
+
...lines.filter((l) => l.price && (!l.price.region?.trim() || !l.price.service?.trim() || !l.price.date || !Number.isFinite(Date.parse(l.price.date)))).map((l) => `${l.component} / ${l.driver}: confirm region, service/SKU and price date.`),
|
|
121
|
+
...lines.filter((l) => l.price?.date && (now - new Date(l.price.date)) / 86400000 > 90).map((l) => `${l.component} / ${l.driver}: price is over 90 days old; review it.`),
|
|
122
|
+
...lines.filter((l) => l.price?.date && new Date(l.price.date) > now).map((l) => `${l.component} / ${l.driver}: price date is in the future.`),
|
|
123
|
+
];
|
|
124
|
+
return {
|
|
125
|
+
lines,
|
|
126
|
+
notCosted,
|
|
127
|
+
usage: u,
|
|
128
|
+
currency: currencies.length === 1 ? currencies[0] : null,
|
|
129
|
+
mixedCurrencies: currencies.length > 1,
|
|
130
|
+
expected: currencies.length > 1 || !costed.length ? null : Math.round(total * 100) / 100,
|
|
131
|
+
low: currencies.length > 1 || !costed.length ? null : Math.round(total * low * 100) / 100,
|
|
132
|
+
high: currencies.length > 1 || !costed.length ? null : Math.round(total * high * 100) / 100,
|
|
133
|
+
factors: { low, high },
|
|
134
|
+
complete: lines.length > 0 && lines.every((l) => l.status === "priced") && !gaps.length,
|
|
135
|
+
drivers: [...costed].sort((a, b) => b.cost - a.cost).slice(0, 3),
|
|
136
|
+
budget: budget ? { amount: budget.value, currency: budgetCurrency, comparable: budgetCurrency === currencies[0] && currencies.length === 1 } : null,
|
|
137
|
+
maturity: costed.length === 0 ? "Not estimated" : lines.every((l) => l.detailed && l.status === "priced") && !notCosted.length && currencies.length === 1 ? "Detailed worksheet" : "ROM",
|
|
138
|
+
gaps,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function setPrice(project, componentId, price) {
|
|
143
|
+
const est = project.estimate || {};
|
|
144
|
+
const editedRate = ('price' in price || 'currency' in price) && !('source' in price);
|
|
145
|
+
const provenance = editedRate ? {source:'entered by you',url:undefined,tierLimit:undefined,note:undefined,date:new Date().toLocaleDateString('en-CA')} : {};
|
|
146
|
+
return {
|
|
147
|
+
...project,
|
|
148
|
+
estimate: {
|
|
149
|
+
...est,
|
|
150
|
+
prices: {
|
|
151
|
+
...(est.prices || {}),
|
|
152
|
+
[componentId]: { source: "entered by you", date: new Date().toLocaleDateString("en-CA"), currency: "USD", ...(est.prices?.[componentId] || {}), ...provenance, ...price },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function setUsage(project, id, value) {
|
|
159
|
+
const est = project.estimate || {};
|
|
160
|
+
const next = { ...(est.usage || {}) };
|
|
161
|
+
if (value === "" || value == null || !Number.isFinite(Number(value))) delete next[id];
|
|
162
|
+
else if (Number(value) >= 0) next[id] = Number(value);
|
|
163
|
+
return { ...project, estimate: { ...est, usage: next } };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const WORK_PHASES = [
|
|
167
|
+
["discovery", "Discovery & specification"], ["design", "Architecture & security"],
|
|
168
|
+
["build", "Implementation & integration"], ["test", "Testing & acceptance"],
|
|
169
|
+
["deploy", "Deployment & handover"],
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
export function consultingEstimate(project) {
|
|
173
|
+
const c = project.estimate?.consulting || {};
|
|
174
|
+
const valid = (v) => Number.isFinite(v) && v >= 0;
|
|
175
|
+
const lines = WORK_PHASES.map(([id, label]) => {
|
|
176
|
+
const l = c.phases?.[id] || {};
|
|
177
|
+
const complete = [l.lowDays, l.days, l.highDays, l.rate].every(valid) && l.lowDays <= l.days && l.days <= l.highDays && !!l.basis?.trim();
|
|
178
|
+
return { id, label, ...l, complete, low: complete ? l.lowDays * l.rate : null, expected: complete ? l.days * l.rate : null, high: complete ? l.highDays * l.rate : null };
|
|
179
|
+
});
|
|
180
|
+
const currencyValid = /^[A-Z]{3}$/.test(c.currency || "USD");
|
|
181
|
+
const sum = (key) => currencyValid && lines.some((l) => l.complete) ? Math.round(lines.reduce((s, l) => s + (l[key] || 0), 0) * 100) / 100 : null;
|
|
182
|
+
return { lines, currency: c.currency || "USD", complete: currencyValid && lines.every((l) => l.complete), expected: sum("expected"), low: sum("low"), high: sum("high") };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function setDetail(project, id, patch) {
|
|
186
|
+
const est = project.estimate || {};
|
|
187
|
+
return { ...project, estimate: { ...est, details: (est.details || []).map((l) => l.id === id ? { ...l, ...patch } : l) } };
|
|
188
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// External ownership is not evidence that an application or integration works.
|
|
2
|
+
// These references never create/import resources or make requests to the endpoint.
|
|
3
|
+
import {externalIdentity,validateExternalReferences} from './external-identities.mjs';
|
|
4
|
+
export function existingReference(node,identity=null) {
|
|
5
|
+
const d=node.data;
|
|
6
|
+
if(!['existing','imported Pulumi state'].includes(d.origin)||d.deploymentMode==='create')return null;
|
|
7
|
+
const explicit=d.binding==='existing-service'||d.origin==='imported Pulumi state'||!!identity;
|
|
8
|
+
if(d.implementation&&!explicit)return null;
|
|
9
|
+
const firebase=!d.binding&&d.provider==='gcp'&&/^(?:Google )?Firebase (?:App )?Hosting$/i.test(String(d.service||'').trim());
|
|
10
|
+
if(!explicit&&!firebase)return null;
|
|
11
|
+
return {id:node.id,label:d.label,provider:d.provider,service:d.service||d.label,ownership:'external',provision:false,import:false,behaviorValidated:false,...(identity?{identity:structuredClone(identity)}:{})};
|
|
12
|
+
}
|
|
13
|
+
export function existingReferenceFiles(project){
|
|
14
|
+
validateExternalReferences(project);
|
|
15
|
+
const references=project.nodes.filter(n=>n.data.kind!=='note').map(n=>existingReference(n,externalIdentity(project,n.id))).filter(Boolean);
|
|
16
|
+
if(!references.length)return {};
|
|
17
|
+
return {'existing-references.json':JSON.stringify({version:1,references},null,2),'EXISTING-SYSTEMS.md':'# Existing systems\n\nThese components remain externally managed. This export does not create, import, replace or modify them. Their presence in the diagram is not proof of availability, authentication, permissions or integration behavior. Connection implementations and runtime tests remain separate requirements.\n\n'+references.map(r=>`- ${JSON.stringify(r.label)} (${JSON.stringify(r.id)}): reference only, preserve existing system.`).join('\n')+'\n'};
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Sanitised provenance of externally managed resources. This is neither an
|
|
2
|
+
// executable import request nor a claim about their native physical IDs/behavior.
|
|
3
|
+
export function validateExternalReferences(project){
|
|
4
|
+
const value=project.extensions?.externalReferences;if(value===undefined)return;
|
|
5
|
+
const plain=v=>!!v&&typeof v==='object'&&!Array.isArray(v)&&[Object.prototype,null].includes(Object.getPrototypeOf(v));
|
|
6
|
+
if(!plain(value)||value.version!==1||Object.keys(value).some(k=>!['version','references'].includes(k))||!Array.isArray(value.references)||value.references.length>200)throw Error('Invalid external reference provenance; preserve the original file.');
|
|
7
|
+
const components=new Set(),identities=new Set();
|
|
8
|
+
for(const entry of value.references){
|
|
9
|
+
if(!plain(entry)||Object.keys(entry).some(k=>!['componentId','source','externalId','resourceType'].includes(k))||entry.source!=='pulumi-stack-export')throw Error('Unsupported external identity provenance; no source was substituted.');
|
|
10
|
+
for(const [key,max]of [['componentId',100],['externalId',600],['resourceType',200]])if(typeof entry[key]!=='string'||!entry[key].trim()||entry[key].length>max)throw Error('Invalid external identity '+key+'; no value was clipped.');
|
|
11
|
+
if(!entry.externalId.startsWith('urn:pulumi:')||components.has(entry.componentId)||identities.has(entry.externalId))throw Error('Duplicate or invalid external identity; resolve ownership without adopting another resource.');
|
|
12
|
+
components.add(entry.componentId);identities.add(entry.externalId);
|
|
13
|
+
const node=project.nodes.find(n=>n.id===entry.componentId);
|
|
14
|
+
if(node&&(!['existing','imported Pulumi state'].includes(node.data.origin)||node.data.deploymentMode==='create'))throw Error('External state provenance identifies a preserved deployment. Keep its reference or model a separate proposed component; do not turn it into new infrastructure.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const externalIdentity=(project,id)=>project.extensions?.externalReferences?.references.find(r=>r.componentId===id)||null;
|