@thebackstoryis/engineering-with-ai 0.2.9
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/Docs/README.md +50 -0
- package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
- package/Docs/adoption/non-technical-team-guide.md +126 -0
- package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
- package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
- package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
- package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
- package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
- package/Docs/cli-reference.md +113 -0
- package/Docs/completed-phase-evidence-amendments.md +74 -0
- package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
- package/Docs/context-aware-delivery-companion-guide.md +198 -0
- package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
- package/Docs/context-management-and-token-efficiency.md +113 -0
- package/Docs/design-systems/design-system-implementation-guide.md +85 -0
- package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
- package/Docs/design-systems/design-system-review-guide.md +51 -0
- package/Docs/design-systems/design-system-user-guide.md +96 -0
- package/Docs/design-systems/product-owner-guide.md +49 -0
- package/Docs/designing-organisation-blueprint-packs.md +384 -0
- package/Docs/developer-delivery-guide.md +224 -0
- package/Docs/error-reporting-guide.md +110 -0
- package/Docs/error-reporting-provider-guide.md +49 -0
- package/Docs/examples/error-report-adapter.md +70 -0
- package/Docs/examples/meeting-review.md +76 -0
- package/Docs/examples/minimal-design-system.md +67 -0
- package/Docs/examples/prototype-review-inputs.md +175 -0
- package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
- package/Docs/examples/test-scenario-input.md +68 -0
- package/Docs/examples/worked-examples.md +147 -0
- package/Docs/existing-project-onboarding-guide.md +214 -0
- package/Docs/explanation/core-concepts.md +26 -0
- package/Docs/explanation/delivery-workflow.md +48 -0
- package/Docs/governance/governance-team-guide.md +139 -0
- package/Docs/governed-starter-project-materialisation-guide.md +284 -0
- package/Docs/guide-catalogue.md +117 -0
- package/Docs/guided-discovery-facilitator-guide.md +172 -0
- package/Docs/guided-intent-workspace-guide.md +109 -0
- package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
- package/Docs/human-approval-and-assurance-guide.md +146 -0
- package/Docs/knowledge-proposals-implementer-guide.md +106 -0
- package/Docs/knowledge-proposals-user-guide.md +247 -0
- package/Docs/maintainers/context-benchmarks.md +29 -0
- package/Docs/maintainers/contributing.md +58 -0
- package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
- package/Docs/maintainers/verification-walkthroughs.md +104 -0
- package/Docs/meeting-evidence-implementer-guide.md +132 -0
- package/Docs/meeting-evidence-user-guide.md +200 -0
- package/Docs/operations/dashboard-and-delivery-state.md +153 -0
- package/Docs/operations/dashboard-configuration.md +87 -0
- package/Docs/operations/installation-updating-and-entitlements.md +135 -0
- package/Docs/operations/premium-personas-setup.md +76 -0
- package/Docs/operations/troubleshooting-and-recovery.md +205 -0
- package/Docs/organisation-rollout-guide.md +142 -0
- package/Docs/persona-entitlement-provider-guide.md +199 -0
- package/Docs/persona-guided-prototype-iteration.md +129 -0
- package/Docs/personas/organisation-specific-personas.md +103 -0
- package/Docs/personas/persona-authoring-cookbook.md +176 -0
- package/Docs/personas/persona-engagement-ui.md +133 -0
- package/Docs/personas/persona-governance.md +118 -0
- package/Docs/platform-export-analysis-guide.md +336 -0
- package/Docs/policies/governance-owner-guide.md +36 -0
- package/Docs/policies/implementation-guide.md +42 -0
- package/Docs/policies/organisation-policy-design-gates.md +58 -0
- package/Docs/policies/policy-pack-authoring-guide.md +108 -0
- package/Docs/policies/product-owner-guide.md +43 -0
- package/Docs/policies/technical-owner-guide.md +37 -0
- package/Docs/product-owner-guide.md +327 -0
- package/Docs/project-portfolio-orchestration-guide.md +199 -0
- package/Docs/quality/manual-qa-and-acceptance.md +162 -0
- package/Docs/quality/persona-driven-test-scenarios.md +172 -0
- package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
- package/Docs/reference/capabilities-and-project-layout.md +678 -0
- package/Docs/reference/cli-and-configuration.md +398 -0
- package/Docs/reference/contributions-api.md +23 -0
- package/Docs/reference/security-adapter-authoring.md +81 -0
- package/Docs/reference/starter-adapter-authoring.md +74 -0
- package/Docs/repository-source-map-guide.md +381 -0
- package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
- package/Docs/screen-prototype-creation-guide.md +324 -0
- package/Docs/security-validation-guide.md +353 -0
- package/Docs/solution-readiness-review-guide.md +123 -0
- package/Docs/standards/project-standards-authoring.md +157 -0
- package/Docs/team-hub-guide.md +162 -0
- package/Docs/team-hub-resource-registry-guide.md +167 -0
- package/Docs/tutorials/first-delivery.md +83 -0
- package/Docs/tutorials/first-session.md +62 -0
- package/Docs/using-lifecycle-hooks.md +381 -0
- package/Docs/working-with-personas.md +274 -0
- package/LICENSE +165 -0
- package/README.md +96 -0
- package/agents-src/claude/ewai-security-reviewer.md +15 -0
- package/bin/ewai +5 -0
- package/config/archaeology-record-families.yaml +59 -0
- package/config/delivery-artifacts.yaml +121 -0
- package/config/delivery-stages.yaml +77 -0
- package/config/design-system.schema.json +46 -0
- package/config/error-reporting.schema.json +79 -0
- package/config/evidence-depth.schema.json +53 -0
- package/config/intent.schema.json +90 -0
- package/config/knowledge-proposals-proposal.schema.json +34 -0
- package/config/lifecycle-event.schema.json +68 -0
- package/config/lifecycle-handler.schema.json +45 -0
- package/config/lifecycle-hook-ack.schema.json +19 -0
- package/config/meeting-evidence-candidate.schema.json +102 -0
- package/config/organisation-policy.schema.json +137 -0
- package/config/pack.schema.json +250 -0
- package/config/persona-pack.schema.json +21 -0
- package/config/persona.schema.json +17 -0
- package/config/policy-evaluation.schema.json +77 -0
- package/config/policy-facts.schema.json +140 -0
- package/config/portfolio.schema.json +68 -0
- package/config/project.schema.json +313 -0
- package/config/prototype-iteration.schema.json +128 -0
- package/config/rollout.schema.json +87 -0
- package/config/security-adapter.schema.json +31 -0
- package/config/security-scan-request.schema.json +64 -0
- package/config/security-scan-response.schema.json +52 -0
- package/config/security-validation-policy.schema.json +74 -0
- package/config/starter-source-acknowledgement.schema.json +13 -0
- package/config/starter-source-adapter.schema.json +38 -0
- package/config/starter-source-request.schema.json +61 -0
- package/package.json +77 -0
- package/packs/core/pack.yaml +7 -0
- package/packs/design-systems/default/experience-promise.md +9 -0
- package/packs/design-systems/default/intentional-review.md +10 -0
- package/packs/design-systems/default/interaction-and-entry.md +9 -0
- package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
- package/packs/design-systems/default/pack.yaml +44 -0
- package/packs/design-systems/default/principles.md +10 -0
- package/packs/personas/core/pack.yaml +7 -0
- package/packs/personas/core/personas/archaeologist.md +37 -0
- package/packs/personas/core/personas/end-user.md +17 -0
- package/packs/personas/core/personas/maintainer.md +17 -0
- package/packs/personas/core/personas/operator.md +17 -0
- package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
- package/packs/technologies/laravel/pack.yaml +30 -0
- package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
- package/packs/technologies/nuxt/pack.yaml +30 -0
- package/packs/technologies/power-platform/pack.yaml +31 -0
- package/packs/technologies/salesforce/pack.yaml +25 -0
- package/public/app.js +4896 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/backstory-icon.png +0 -0
- package/public/dashboard-navigation.js +98 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +789 -0
- package/public/styles.css +2693 -0
- package/public/team-hub/app.js +202 -0
- package/public/team-hub/index.html +79 -0
- package/public/team-hub/styles.css +90 -0
- package/scripts/publication-check.mjs +140 -0
- package/scripts/setup.mjs +21 -0
- package/skills-src/ewai-archaeology/SKILL.md +334 -0
- package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
- package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
- package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
- package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
- package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
- package/skills-src/ewai-architecture/SKILL.md +108 -0
- package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
- package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
- package/skills-src/ewai-context/SKILL.md +68 -0
- package/skills-src/ewai-context/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/SKILL.md +118 -0
- package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
- package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
- package/skills-src/ewai-deliver/SKILL.md +122 -0
- package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
- package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
- package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
- package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
- package/skills-src/ewai-design-system-author/SKILL.md +28 -0
- package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
- package/skills-src/ewai-design-system-review/SKILL.md +26 -0
- package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
- package/skills-src/ewai-error-reporting/SKILL.md +46 -0
- package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
- package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
- package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
- package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
- package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
- package/skills-src/ewai-intent/SKILL.md +68 -0
- package/skills-src/ewai-intent/agents/openai.yaml +4 -0
- package/skills-src/ewai-intent/references/intent-contract.md +42 -0
- package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
- package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
- package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
- package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
- package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
- package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
- package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
- package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
- package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
- package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
- package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
- package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
- package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
- package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/SKILL.md +130 -0
- package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/references/cli.md +86 -0
- package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
- package/skills-src/ewai-portfolio/SKILL.md +70 -0
- package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
- package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
- package/skills-src/ewai-project-discovery/SKILL.md +95 -0
- package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
- package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
- package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
- package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
- package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
- package/skills-src/ewai-retro/SKILL.md +48 -0
- package/skills-src/ewai-retro/agents/openai.yaml +4 -0
- package/skills-src/ewai-retro/references/asset-routing.md +14 -0
- package/skills-src/ewai-rollout/SKILL.md +74 -0
- package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
- package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
- package/skills-src/ewai-shape-intents/SKILL.md +84 -0
- package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
- package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
- package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
- package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/SKILL.md +93 -0
- package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
- package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
- package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
- package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
- package/src/afk-worker.mjs +16 -0
- package/src/archaeology.mjs +1333 -0
- package/src/checkin.mjs +261 -0
- package/src/cli.mjs +2427 -0
- package/src/companion-guidance.mjs +257 -0
- package/src/companion-opening.mjs +62 -0
- package/src/companion.mjs +256 -0
- package/src/context.mjs +210 -0
- package/src/dashboard-preferences.mjs +80 -0
- package/src/delivery-artifacts.mjs +204 -0
- package/src/delivery-documents.mjs +248 -0
- package/src/delivery-gates.mjs +317 -0
- package/src/delivery.mjs +1433 -0
- package/src/design-system-application.mjs +291 -0
- package/src/design-system-authoring.mjs +101 -0
- package/src/design-systems.mjs +466 -0
- package/src/discovery.mjs +1314 -0
- package/src/error-reporting.mjs +323 -0
- package/src/evidence-depth.mjs +543 -0
- package/src/execution-state.mjs +243 -0
- package/src/install.mjs +166 -0
- package/src/intent-dependencies.mjs +117 -0
- package/src/intent-maps.mjs +402 -0
- package/src/intents.mjs +747 -0
- package/src/knowledge-proposals.mjs +717 -0
- package/src/launcher.mjs +51 -0
- package/src/meeting-evidence.mjs +703 -0
- package/src/network-rollout.mjs +386 -0
- package/src/organisation-blueprints.mjs +438 -0
- package/src/organisation-policies.mjs +448 -0
- package/src/packs.mjs +44 -0
- package/src/paths.mjs +62 -0
- package/src/persona-entitlements.mjs +438 -0
- package/src/persona-licence-config.mjs +98 -0
- package/src/persona-website-provider.mjs +134 -0
- package/src/persona-zip.mjs +87 -0
- package/src/personas.mjs +159 -0
- package/src/platform-metadata-analysis.mjs +314 -0
- package/src/policy-design-gates.mjs +623 -0
- package/src/policy-gate-integration.mjs +318 -0
- package/src/portfolio.mjs +509 -0
- package/src/power-platform-source-map.mjs +190 -0
- package/src/project.mjs +449 -0
- package/src/prototype-iterations.mjs +730 -0
- package/src/repository-source-map.mjs +603 -0
- package/src/runtime/afk-conductor.mjs +973 -0
- package/src/runtime/context-assembly.mjs +457 -0
- package/src/runtime/context-benchmarks.mjs +115 -0
- package/src/runtime/dashboard-actions.mjs +109 -0
- package/src/runtime/dashboard-handoffs.mjs +149 -0
- package/src/runtime/dashboard-server.mjs +1272 -0
- package/src/runtime/dashboard.mjs +197 -0
- package/src/runtime/database.mjs +789 -0
- package/src/runtime/error-reporting.mjs +581 -0
- package/src/runtime/evidence-depth-workspace.mjs +412 -0
- package/src/runtime/execution-leases.mjs +299 -0
- package/src/runtime/guided-discovery.mjs +350 -0
- package/src/runtime/guided-intents.mjs +517 -0
- package/src/runtime/impact-analysis.mjs +535 -0
- package/src/runtime/intents.mjs +222 -0
- package/src/runtime/knowledge.mjs +86 -0
- package/src/runtime/lifecycle-hooks.mjs +1239 -0
- package/src/runtime/mcp-config.mjs +110 -0
- package/src/runtime/mcp-server.mjs +1885 -0
- package/src/runtime/palace.mjs +362 -0
- package/src/runtime/paths.mjs +58 -0
- package/src/runtime/persona-engagement.mjs +255 -0
- package/src/runtime/phase-contributions.mjs +594 -0
- package/src/runtime/policy-workspace.mjs +170 -0
- package/src/runtime/prototype-iterations.mjs +235 -0
- package/src/runtime/provider-adapters.mjs +163 -0
- package/src/runtime/repository-index.mjs +838 -0
- package/src/runtime/runs.mjs +185 -0
- package/src/runtime/security-validation.mjs +1230 -0
- package/src/runtime/starter-materialisation.mjs +1155 -0
- package/src/runtime/team-hub-client.mjs +479 -0
- package/src/runtime/team-hub-database.mjs +288 -0
- package/src/runtime/team-hub-server.mjs +191 -0
- package/src/runtime/team-hub.mjs +110 -0
- package/src/runtime/tree-sitter-index.mjs +390 -0
- package/src/runtime/version.mjs +1 -0
- package/src/runtime/work.mjs +633 -0
- package/src/salesforce-source-map.mjs +212 -0
- package/src/security-validation-config.mjs +224 -0
- package/src/solution-readiness.mjs +620 -0
- package/src/starter-materialisation-contract.mjs +407 -0
- package/src/task-graph.mjs +544 -0
- package/src/team-hub-resources.mjs +239 -0
- package/src/team-hub.mjs +242 -0
- package/src/test-scenarios.mjs +622 -0
- package/src/validation-config.mjs +289 -0
- package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
- package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
- package/templates/SPECS/6.Build/_tracker-template.md +16 -0
- package/templates/SPECS/pipeline.yaml +62 -0
- package/templates/discovery-answers.yaml +86 -0
- package/templates/intent-body.md +21 -0
- package/tests/fixtures/context-benchmarks.json +9 -0
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
existsSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
linkSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
rmdirSync,
|
|
18
|
+
unlinkSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} from 'node:fs';
|
|
21
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
22
|
+
import {
|
|
23
|
+
STARTER_ADAPTER_TRUST_NOTICE,
|
|
24
|
+
STARTER_MATERIALISATION_NOTICE,
|
|
25
|
+
canonicalStarterTree,
|
|
26
|
+
classifyStarterSource,
|
|
27
|
+
resolveStarterTargetMappings,
|
|
28
|
+
starterPackDigest,
|
|
29
|
+
starterProjectRevision,
|
|
30
|
+
starterTreeDigest,
|
|
31
|
+
validateStarterAdapterPackage,
|
|
32
|
+
} from '../starter-materialisation-contract.mjs';
|
|
33
|
+
import {
|
|
34
|
+
listOrganisationBlueprints,
|
|
35
|
+
resolveOrganisationBlueprint,
|
|
36
|
+
} from '../organisation-blueprints.mjs';
|
|
37
|
+
import { loadProjectConfig } from '../project.mjs';
|
|
38
|
+
import { openRuntimeDatabase } from './database.mjs';
|
|
39
|
+
import { publishLifecycleEventSafely, resolveLifecyclePersonas } from './lifecycle-hooks.mjs';
|
|
40
|
+
import { runtimePaths } from './paths.mjs';
|
|
41
|
+
|
|
42
|
+
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
|
43
|
+
const rolePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
44
|
+
const activeAttemptStates = ['preparing', 'prepared', 'applying', 'recovery-required'];
|
|
45
|
+
const previewLifetimeMs = 30 * 60 * 1_000;
|
|
46
|
+
const protectedRoots = new Set(['.git', '.ewai-pipeline', '.codex', '.claude', '.agents']);
|
|
47
|
+
const protectedFiles = new Set(['agents.md', 'claude.md', '.mcp.json', '.env', '.npmrc', '.netrc']);
|
|
48
|
+
const defaultLimits = Object.freeze({
|
|
49
|
+
maximumDepth: 24,
|
|
50
|
+
maximumPathBytes: 512,
|
|
51
|
+
maximumFiles: 5_000,
|
|
52
|
+
maximumFileBytes: 5 * 1024 * 1024,
|
|
53
|
+
maximumTotalBytes: 100 * 1024 * 1024,
|
|
54
|
+
timeoutMs: 5 * 60 * 1_000,
|
|
55
|
+
maximumOutputBytes: 64 * 1024,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function now(options = {}) {
|
|
59
|
+
const value = typeof options.now === 'function' ? options.now() : options.now;
|
|
60
|
+
return new Date(value ?? Date.now()).toISOString();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sha256(value) {
|
|
64
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseJson(value, fallback) {
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(value);
|
|
70
|
+
} catch {
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function within(root, candidate) {
|
|
76
|
+
const path = relative(root, candidate);
|
|
77
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function snapshotMappings(mappings) {
|
|
81
|
+
return mappings.map(({ role, repository, repositoryRole, repositoryPath, path, sourcePath }) => ({
|
|
82
|
+
role,
|
|
83
|
+
repository,
|
|
84
|
+
repositoryRole,
|
|
85
|
+
repositoryPath,
|
|
86
|
+
path,
|
|
87
|
+
sourcePath,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sameJson(left, right) {
|
|
92
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function protectedDestination(path, specsPath, repositoryPath) {
|
|
96
|
+
const normalised = String(path).replaceAll('\\', '/').normalize('NFC');
|
|
97
|
+
const segments = normalised.split('/').map((segment) => segment.toLocaleLowerCase('en-US'));
|
|
98
|
+
if (segments.some((segment) => protectedRoots.has(segment)) || protectedFiles.has(segments.at(-1))) return true;
|
|
99
|
+
const destination = resolve(repositoryPath, normalised);
|
|
100
|
+
return destination === specsPath || within(specsPath, destination);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertBoundedDestination(projectRoot, mapping, destinationPath) {
|
|
104
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
105
|
+
const repositoryRoot = realpathSync(mapping.repositoryRoot);
|
|
106
|
+
const destination = resolve(repositoryRoot, destinationPath);
|
|
107
|
+
if (!within(repositoryRoot, destination)) throw new Error(`Starter destination escapes repository ${mapping.repository}: ${destinationPath}`);
|
|
108
|
+
const specsRoot = existsSync(paths.specsRoot) ? realpathSync(paths.specsRoot) : resolve(paths.specsRoot);
|
|
109
|
+
if (protectedDestination(destinationPath, specsRoot, repositoryRoot)) {
|
|
110
|
+
throw new Error(`Starter destination resolves into protected project content: ${destinationPath}`);
|
|
111
|
+
}
|
|
112
|
+
let cursor = repositoryRoot;
|
|
113
|
+
for (const segment of destinationPath.split('/')) {
|
|
114
|
+
const parent = cursor;
|
|
115
|
+
cursor = resolve(cursor, segment);
|
|
116
|
+
if (!within(repositoryRoot, cursor)) throw new Error(`Starter destination escapes repository ${mapping.repository}: ${destinationPath}`);
|
|
117
|
+
if (!existsSync(cursor)) continue;
|
|
118
|
+
const metadata = lstatSync(cursor);
|
|
119
|
+
if (metadata.isSymbolicLink()) throw new Error(`Starter destination may not traverse a symbolic link: ${destinationPath}`);
|
|
120
|
+
const equivalent = readdirSync(parent).find((name) => name.normalize('NFC').toLocaleLowerCase('en-US') === segment.normalize('NFC').toLocaleLowerCase('en-US'));
|
|
121
|
+
if (equivalent && equivalent !== segment) throw new Error(`Starter destination has a case or Unicode-equivalent collision: ${destinationPath}`);
|
|
122
|
+
}
|
|
123
|
+
return destination;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function regularFileDigest(path) {
|
|
127
|
+
if (!existsSync(path)) return null;
|
|
128
|
+
const metadata = lstatSync(path);
|
|
129
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) return null;
|
|
130
|
+
return sha256(readFileSync(path));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function classifyDestinations(projectRoot, mappings, files) {
|
|
134
|
+
return files.map((file) => {
|
|
135
|
+
const mapping = mappings.find((candidate) => candidate.role === file.target_role);
|
|
136
|
+
if (!mapping) throw new Error(`Starter target mapping disappeared: ${file.target_role}`);
|
|
137
|
+
const destination = assertBoundedDestination(projectRoot, mapping, file.destination_path);
|
|
138
|
+
let classification = 'create';
|
|
139
|
+
if (existsSync(destination)) classification = regularFileDigest(destination) === file.content_digest ? 'identical' : 'conflict';
|
|
140
|
+
return { ...file, destination, classification };
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function classificationCounts(files) {
|
|
145
|
+
return files.reduce((counts, file) => {
|
|
146
|
+
counts[file.classification] += 1;
|
|
147
|
+
return counts;
|
|
148
|
+
}, { create: 0, identical: 0, conflict: 0 });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function previewBinding({ previewId, attemptId, receipt, adapter, mappings, repositoryRevisions, treeDigest, targetTreeDigests, files, expiresAt }) {
|
|
152
|
+
return {
|
|
153
|
+
schema: 'ewai.starter-materialisation-preview-binding/v1',
|
|
154
|
+
previewId,
|
|
155
|
+
attemptId,
|
|
156
|
+
receipt,
|
|
157
|
+
adapter,
|
|
158
|
+
mappings: snapshotMappings(mappings),
|
|
159
|
+
repositoryRevisions,
|
|
160
|
+
treeDigest,
|
|
161
|
+
targetTreeDigests,
|
|
162
|
+
destinations: files.map(({ target_role, repository_name, destination_path, content_digest, size_bytes, classification }) => ({
|
|
163
|
+
targetRole: target_role,
|
|
164
|
+
repository: repository_name,
|
|
165
|
+
path: destination_path,
|
|
166
|
+
digest: content_digest,
|
|
167
|
+
bytes: size_bytes,
|
|
168
|
+
classification,
|
|
169
|
+
})),
|
|
170
|
+
expiresAt,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function strictObject(value, allowed, label) {
|
|
175
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
176
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${label} contains unknown field: ${key}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function safeName(value, label, maximum = 240) {
|
|
180
|
+
const text = String(value ?? '').trim();
|
|
181
|
+
if (!text || text.length > maximum || /[\u0000-\u001f\u007f]/.test(text)) throw new Error(`${label} is unsafe.`);
|
|
182
|
+
return text;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sameValues(left, right) {
|
|
186
|
+
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function receiptTargets(item) {
|
|
190
|
+
if (!Array.isArray(item.targets) || !item.targets.length) throw new Error(`Governed Starter Pack ${item.id} declares no logical targets.`);
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
const targets = item.targets.map((target) => {
|
|
193
|
+
const role = safeName(target.role, 'Governed Starter Pack target role', 80);
|
|
194
|
+
const sourcePath = safeName(target.sourcePath, `Governed Starter Pack ${role} source path`, 512).replaceAll('\\', '/');
|
|
195
|
+
if (!rolePattern.test(role) || seen.has(role)) throw new Error(`Governed Starter Pack target role is invalid or duplicated: ${role}`);
|
|
196
|
+
seen.add(role);
|
|
197
|
+
if (sourcePath.startsWith('/') || sourcePath.startsWith('../') || sourcePath.includes('/../') || sourcePath.includes('\0')) {
|
|
198
|
+
throw new Error(`Governed Starter Pack target source path is unsafe: ${sourcePath}`);
|
|
199
|
+
}
|
|
200
|
+
return { role, sourcePath };
|
|
201
|
+
});
|
|
202
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
203
|
+
for (let other = index + 1; other < targets.length; other += 1) {
|
|
204
|
+
const left = targets[index].sourcePath;
|
|
205
|
+
const right = targets[other].sourcePath;
|
|
206
|
+
if (left === right || left === '.' || right === '.' || left.startsWith(`${right}/`) || right.startsWith(`${left}/`)) {
|
|
207
|
+
throw new Error(`Governed Starter Pack source target paths overlap: ${left} and ${right}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return targets.sort((left, right) => left.role.localeCompare(right.role));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function safeReceipt(pack, module, item, selection) {
|
|
215
|
+
const sourceClass = classifyStarterSource(item.source);
|
|
216
|
+
if (!digestPattern.test(item.digest)) throw new Error(`Governed Starter Pack ${item.id} requires a canonical SHA-256 digest.`);
|
|
217
|
+
return {
|
|
218
|
+
id: `${pack.id}:${module.id}:${item.id}`,
|
|
219
|
+
name: safeName(item.name, 'Governed Starter Pack name'),
|
|
220
|
+
source: safeName(item.source, 'Governed Starter Pack source', 2_048),
|
|
221
|
+
sourceClass,
|
|
222
|
+
version: safeName(item.version, 'Governed Starter Pack version', 80),
|
|
223
|
+
digest: item.digest,
|
|
224
|
+
licence: safeName(item.licence, 'Governed Starter Pack licence'),
|
|
225
|
+
compatibility: safeName(item.compatibility, 'Governed Starter Pack compatibility', 500),
|
|
226
|
+
targets: receiptTargets(item),
|
|
227
|
+
pack: { id: pack.id, version: pack.version, digest: pack.digest },
|
|
228
|
+
module: { id: module.id, name: module.name },
|
|
229
|
+
accepted: {
|
|
230
|
+
selectionDigest: selection.selection_digest,
|
|
231
|
+
approvedBy: selection.approved_by,
|
|
232
|
+
approvedAt: selection.approved_at,
|
|
233
|
+
evidence: selection.evidence,
|
|
234
|
+
},
|
|
235
|
+
...(item.legacy ? { legacy: true } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function resolveAcceptedStarterReceipts(projectRoot, options = {}) {
|
|
240
|
+
const { config } = loadProjectConfig(projectRoot);
|
|
241
|
+
const selection = config.blueprints?.organisation;
|
|
242
|
+
if (!selection) throw new Error('No accepted Organisation Blueprint is configured for this project.');
|
|
243
|
+
const catalogue = listOrganisationBlueprints({
|
|
244
|
+
projectRoot,
|
|
245
|
+
...(options.roots ? { roots: options.roots } : {}),
|
|
246
|
+
...(options.home ? { home: options.home } : {}),
|
|
247
|
+
});
|
|
248
|
+
const resolved = resolveOrganisationBlueprint(selection.root.id, catalogue, { enabledModules: selection.enabled_modules });
|
|
249
|
+
if (
|
|
250
|
+
resolved.root.version !== selection.root.version
|
|
251
|
+
|| resolved.root.digest !== selection.root.digest
|
|
252
|
+
|| resolved.digest !== selection.selection_digest
|
|
253
|
+
) throw new Error('Accepted Organisation Blueprint root or selection has drifted.');
|
|
254
|
+
const currentPins = resolved.packs.map((pack) => `${pack.id}@${pack.version}:${pack.digest}`);
|
|
255
|
+
const acceptedPins = selection.packs.map((pack) => `${pack.id}@${pack.version}:${pack.digest}`);
|
|
256
|
+
if (!sameValues(currentPins, acceptedPins)) throw new Error('Accepted Organisation Blueprint pack set has drifted.');
|
|
257
|
+
const currentModules = resolved.modules.map(({ packId, module }) => `${packId}:${module.id}`);
|
|
258
|
+
if (!sameValues(currentModules, selection.applied_modules)) throw new Error('Accepted Organisation Blueprint module set has drifted.');
|
|
259
|
+
const receipts = resolved.modules.flatMap(({ packId, module }) => {
|
|
260
|
+
const pack = resolved.packs.find((candidate) => candidate.id === packId);
|
|
261
|
+
return module.starterPacks.map((item) => safeReceipt(pack, module, item, selection));
|
|
262
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
263
|
+
const duplicates = receipts.find((receipt, index) => receipts.findIndex((candidate) => candidate.id === receipt.id) !== index);
|
|
264
|
+
if (duplicates) throw new Error(`Accepted Governed Starter Pack receipt is duplicated: ${duplicates.id}`);
|
|
265
|
+
if (options.receiptId) {
|
|
266
|
+
const receipt = receipts.find((candidate) => candidate.id === options.receiptId);
|
|
267
|
+
if (!receipt) throw new Error(`Unknown accepted Governed Starter Pack receipt: ${options.receiptId}`);
|
|
268
|
+
return [receipt];
|
|
269
|
+
}
|
|
270
|
+
return receipts;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function safeAdapter(row) {
|
|
274
|
+
return {
|
|
275
|
+
id: row.id,
|
|
276
|
+
name: row.name,
|
|
277
|
+
publisher: { id: row.publisher_id, name: row.publisher_name },
|
|
278
|
+
version: row.version,
|
|
279
|
+
protocolVersion: row.protocol_version,
|
|
280
|
+
sourceClasses: JSON.parse(row.source_classes_json),
|
|
281
|
+
manifestDigest: row.manifest_digest,
|
|
282
|
+
packageDigest: row.package_digest,
|
|
283
|
+
registeredAt: row.registered_at,
|
|
284
|
+
updatedAt: row.updated_at,
|
|
285
|
+
notice: STARTER_ADAPTER_TRUST_NOTICE,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function listStarterAdapters(projectRoot) {
|
|
290
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
291
|
+
try {
|
|
292
|
+
return database.prepare('SELECT * FROM starter_adapters ORDER BY id').all().map(safeAdapter);
|
|
293
|
+
} finally {
|
|
294
|
+
database.close();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function registerStarterAdapter(projectRoot, folder, input = {}) {
|
|
299
|
+
if (input.confirmed !== true) throw new Error('Starter adapter registration requires explicit confirmation.');
|
|
300
|
+
const adapter = validateStarterAdapterPackage(folder, input);
|
|
301
|
+
const at = now(input);
|
|
302
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
303
|
+
try {
|
|
304
|
+
const existing = database.prepare('SELECT * FROM starter_adapters WHERE id = ?').get(adapter.id);
|
|
305
|
+
if (existing && (existing.publisher_id !== adapter.publisher.id || existing.publisher_name !== adapter.publisher.name)) {
|
|
306
|
+
throw new Error(`Starter adapter ${adapter.id} publisher identity has drifted.`);
|
|
307
|
+
}
|
|
308
|
+
if (existing && existing.version === adapter.version && existing.package_digest !== adapter.packageDigest) {
|
|
309
|
+
throw new Error(`Starter adapter ${adapter.id} has same-version package digest drift.`);
|
|
310
|
+
}
|
|
311
|
+
database.prepare(`
|
|
312
|
+
INSERT INTO starter_adapters (
|
|
313
|
+
id, name, publisher_id, publisher_name, version, protocol_version,
|
|
314
|
+
source_classes_json, trusted_root, entrypoint, manifest_digest, package_digest,
|
|
315
|
+
manifest_json, registered_at, updated_at
|
|
316
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
317
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
318
|
+
name = excluded.name,
|
|
319
|
+
publisher_id = excluded.publisher_id,
|
|
320
|
+
publisher_name = excluded.publisher_name,
|
|
321
|
+
version = excluded.version,
|
|
322
|
+
protocol_version = excluded.protocol_version,
|
|
323
|
+
source_classes_json = excluded.source_classes_json,
|
|
324
|
+
trusted_root = excluded.trusted_root,
|
|
325
|
+
entrypoint = excluded.entrypoint,
|
|
326
|
+
manifest_digest = excluded.manifest_digest,
|
|
327
|
+
package_digest = excluded.package_digest,
|
|
328
|
+
manifest_json = excluded.manifest_json,
|
|
329
|
+
updated_at = excluded.updated_at
|
|
330
|
+
`).run(
|
|
331
|
+
adapter.id,
|
|
332
|
+
adapter.name,
|
|
333
|
+
adapter.publisher.id,
|
|
334
|
+
adapter.publisher.name,
|
|
335
|
+
adapter.version,
|
|
336
|
+
adapter.protocolVersion,
|
|
337
|
+
JSON.stringify(adapter.sourceClasses),
|
|
338
|
+
adapter.trustedRoot,
|
|
339
|
+
adapter.entrypoint,
|
|
340
|
+
adapter.manifestDigest,
|
|
341
|
+
adapter.packageDigest,
|
|
342
|
+
JSON.stringify({
|
|
343
|
+
schema: adapter.schema,
|
|
344
|
+
id: adapter.id,
|
|
345
|
+
name: adapter.name,
|
|
346
|
+
publisher: adapter.publisher,
|
|
347
|
+
version: adapter.version,
|
|
348
|
+
protocolVersion: adapter.protocolVersion,
|
|
349
|
+
sourceClasses: adapter.sourceClasses,
|
|
350
|
+
entrypoint: adapter.entrypoint,
|
|
351
|
+
}),
|
|
352
|
+
existing?.registered_at ?? at,
|
|
353
|
+
at,
|
|
354
|
+
);
|
|
355
|
+
return safeAdapter(database.prepare('SELECT * FROM starter_adapters WHERE id = ?').get(adapter.id));
|
|
356
|
+
} finally {
|
|
357
|
+
database.close();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function revalidateAdapter(row) {
|
|
362
|
+
const current = validateStarterAdapterPackage(row.trusted_root);
|
|
363
|
+
for (const [field, left, right] of [
|
|
364
|
+
['identity', current.id, row.id],
|
|
365
|
+
['version', current.version, row.version],
|
|
366
|
+
['entrypoint', current.entrypoint, row.entrypoint],
|
|
367
|
+
['manifest digest', current.manifestDigest, row.manifest_digest],
|
|
368
|
+
['package digest', current.packageDigest, row.package_digest],
|
|
369
|
+
]) {
|
|
370
|
+
if (left !== right) throw new Error(`Registered starter adapter ${field} has drifted.`);
|
|
371
|
+
}
|
|
372
|
+
return current;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function validateAcknowledgement(value, expected) {
|
|
376
|
+
strictObject(value, new Set(['schema', 'attemptId', 'nonce', 'status']), 'Starter adapter acknowledgement');
|
|
377
|
+
if (value.schema !== 'ewai.starter-source-acknowledgement/v1') throw new Error('Unsupported starter adapter acknowledgement schema.');
|
|
378
|
+
if (value.attemptId !== expected.attemptId || value.nonce !== expected.nonce) throw new Error('Starter adapter acknowledgement identity does not match the request.');
|
|
379
|
+
if (value.status !== 'prepared') throw new Error('Starter adapter acknowledgement did not report prepared status.');
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function runAdapter(adapter, request, limits) {
|
|
384
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
385
|
+
const child = spawn(adapter.entrypointPath, [], {
|
|
386
|
+
cwd: request.stagingRoot,
|
|
387
|
+
shell: false,
|
|
388
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
389
|
+
env: {
|
|
390
|
+
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
391
|
+
LANG: 'C',
|
|
392
|
+
LC_ALL: 'C',
|
|
393
|
+
EWAI_STARTER_ADAPTER_PROTOCOL: '1',
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
const stdout = [];
|
|
397
|
+
let outputBytes = 0;
|
|
398
|
+
let overflow = false;
|
|
399
|
+
let timedOut = false;
|
|
400
|
+
const consume = (chunk, capture) => {
|
|
401
|
+
outputBytes += chunk.length;
|
|
402
|
+
if (outputBytes > limits.maximumOutputBytes) {
|
|
403
|
+
overflow = true;
|
|
404
|
+
child.kill('SIGKILL');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (capture) stdout.push(chunk);
|
|
408
|
+
};
|
|
409
|
+
child.stdout.on('data', (chunk) => consume(chunk, true));
|
|
410
|
+
child.stderr.on('data', (chunk) => consume(chunk, false));
|
|
411
|
+
child.on('error', rejectRun);
|
|
412
|
+
const timer = setTimeout(() => {
|
|
413
|
+
timedOut = true;
|
|
414
|
+
child.kill('SIGKILL');
|
|
415
|
+
}, limits.timeoutMs);
|
|
416
|
+
child.on('close', (code, signal) => {
|
|
417
|
+
clearTimeout(timer);
|
|
418
|
+
if (timedOut) return rejectRun(new Error('Starter adapter timed out.'));
|
|
419
|
+
if (overflow) return rejectRun(new Error('Starter adapter exceeded its output-byte limit.'));
|
|
420
|
+
if (code !== 0) return rejectRun(new Error(`Starter adapter failed with ${signal ? `signal ${signal}` : `exit code ${code}`}.`));
|
|
421
|
+
let acknowledgement;
|
|
422
|
+
try {
|
|
423
|
+
acknowledgement = JSON.parse(Buffer.concat(stdout).toString('utf8'));
|
|
424
|
+
} catch {
|
|
425
|
+
return rejectRun(new Error('Starter adapter returned a malformed acknowledgement.'));
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
resolveRun(validateAcknowledgement(acknowledgement, request));
|
|
429
|
+
} catch (error) {
|
|
430
|
+
rejectRun(error);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
child.stdin.end(`${JSON.stringify(request)}\n`);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function boundedStagingRoot(projectRoot, attemptId) {
|
|
438
|
+
const paths = runtimePaths(projectRoot);
|
|
439
|
+
const realRuntimeRoot = realpathSync(paths.runtimeRoot);
|
|
440
|
+
let cursor = realRuntimeRoot;
|
|
441
|
+
for (const segment of ['starter-materialisation', 'staging']) {
|
|
442
|
+
cursor = resolve(cursor, segment);
|
|
443
|
+
if (!existsSync(cursor)) mkdirSync(cursor, { mode: 0o700 });
|
|
444
|
+
if (lstatSync(cursor).isSymbolicLink() || !lstatSync(cursor).isDirectory()) throw new Error('Starter staging path may not traverse symbolic or non-directory content.');
|
|
445
|
+
}
|
|
446
|
+
const realStagingRoot = realpathSync(cursor);
|
|
447
|
+
const runtimeBound = relative(realRuntimeRoot, realStagingRoot);
|
|
448
|
+
if (!runtimeBound || runtimeBound.startsWith('..') || isAbsolute(runtimeBound)) throw new Error('Starter staging root escapes its project runtime root.');
|
|
449
|
+
const stagingRoot = resolve(realStagingRoot, attemptId);
|
|
450
|
+
const bounded = relative(realStagingRoot, stagingRoot);
|
|
451
|
+
if (!bounded || bounded.startsWith('..') || isAbsolute(bounded)) throw new Error('Starter staging path escapes its project runtime root.');
|
|
452
|
+
mkdirSync(stagingRoot, { recursive: false, mode: 0o700 });
|
|
453
|
+
return realpathSync(stagingRoot);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function relativeDestination(mapping, filePath) {
|
|
457
|
+
return mapping.path === '.' ? filePath : `${mapping.path}/${filePath}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function safePreparedProjection(attempt, mappings, targetTrees, files) {
|
|
461
|
+
const counts = classificationCounts(files);
|
|
462
|
+
return {
|
|
463
|
+
schema: 'ewai.starter-materialisation-preview/v1',
|
|
464
|
+
attemptId: attempt.id,
|
|
465
|
+
previewId: attempt.previewId,
|
|
466
|
+
previewDigest: attempt.previewDigest,
|
|
467
|
+
expiresAt: attempt.expiresAt,
|
|
468
|
+
status: 'prepared',
|
|
469
|
+
receiptId: attempt.receiptId,
|
|
470
|
+
adapterId: attempt.adapterId,
|
|
471
|
+
treeDigest: attempt.treeDigest,
|
|
472
|
+
files: targetTrees.reduce((count, target) => count + target.files.length, 0),
|
|
473
|
+
bytes: targetTrees.reduce((count, target) => count + target.files.reduce((sum, file) => sum + file.bytes, 0), 0),
|
|
474
|
+
classifications: counts,
|
|
475
|
+
targets: targetTrees.map((target) => {
|
|
476
|
+
const mapping = mappings.find((candidate) => candidate.role === target.role);
|
|
477
|
+
return {
|
|
478
|
+
role: target.role,
|
|
479
|
+
repository: mapping.repository,
|
|
480
|
+
path: mapping.path,
|
|
481
|
+
files: target.files.length,
|
|
482
|
+
bytes: target.files.reduce((sum, file) => sum + file.bytes, 0),
|
|
483
|
+
digest: target.digest,
|
|
484
|
+
classifications: classificationCounts(files.filter((file) => file.target_role === target.role)),
|
|
485
|
+
};
|
|
486
|
+
}),
|
|
487
|
+
projectRevision: attempt.repositoryRevisions.digest,
|
|
488
|
+
notice: STARTER_MATERIALISATION_NOTICE,
|
|
489
|
+
adapterNotice: STARTER_ADAPTER_TRUST_NOTICE,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function prepareStarterMaterialisation(projectRoot, receiptId, adapterId, input = {}) {
|
|
494
|
+
if (input.confirmed !== true) throw new Error('Starter materialisation preparation requires explicit confirmation.');
|
|
495
|
+
const timeoutMs = Number(input.timeoutMs ?? defaultLimits.timeoutMs);
|
|
496
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 15 * 60 * 1_000) throw new Error('Starter adapter timeout must be between 1000 and 900000 milliseconds.');
|
|
497
|
+
const limits = { ...defaultLimits, timeoutMs };
|
|
498
|
+
const [receipt] = resolveAcceptedStarterReceipts(projectRoot, { ...input, receiptId });
|
|
499
|
+
const mappings = resolveStarterTargetMappings(projectRoot, receipt);
|
|
500
|
+
const repositoryRevisions = starterProjectRevision(mappings);
|
|
501
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
502
|
+
const attemptId = randomUUID();
|
|
503
|
+
const nonce = randomUUID();
|
|
504
|
+
const createdAt = now(input);
|
|
505
|
+
let stagingRoot = '';
|
|
506
|
+
try {
|
|
507
|
+
database.prepare(`
|
|
508
|
+
UPDATE starter_attempts SET status = 'expired', error_code = 'preview-expired', updated_at = ?
|
|
509
|
+
WHERE status = 'prepared' AND expires_at IS NOT NULL AND expires_at < ?
|
|
510
|
+
`).run(createdAt, createdAt);
|
|
511
|
+
const active = database.prepare(`SELECT id FROM starter_attempts WHERE status IN (${activeAttemptStates.map(() => '?').join(',')}) ORDER BY created_at LIMIT 1`).get(...activeAttemptStates);
|
|
512
|
+
if (active) throw new Error(`Starter materialisation is busy with attempt ${active.id}.`);
|
|
513
|
+
const row = database.prepare('SELECT * FROM starter_adapters WHERE id = ?').get(adapterId);
|
|
514
|
+
if (!row) throw new Error(`Unknown registered starter adapter: ${adapterId}`);
|
|
515
|
+
const adapter = revalidateAdapter(row);
|
|
516
|
+
if (!adapter.sourceClasses.includes(receipt.sourceClass)) throw new Error(`Starter adapter ${adapter.id} does not support source class ${receipt.sourceClass}.`);
|
|
517
|
+
stagingRoot = boundedStagingRoot(projectRoot, attemptId);
|
|
518
|
+
const adapterSnapshot = safeAdapter(row);
|
|
519
|
+
database.prepare(`
|
|
520
|
+
INSERT INTO starter_attempts (
|
|
521
|
+
id, receipt_id, adapter_id, status, nonce, staging_root, receipt_json,
|
|
522
|
+
target_mappings_json, adapter_snapshot_json, limits_json,
|
|
523
|
+
repository_revisions_json, created_at, updated_at
|
|
524
|
+
) VALUES (?, ?, ?, 'preparing', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
525
|
+
`).run(
|
|
526
|
+
attemptId,
|
|
527
|
+
receipt.id,
|
|
528
|
+
adapter.id,
|
|
529
|
+
nonce,
|
|
530
|
+
stagingRoot,
|
|
531
|
+
JSON.stringify(receipt),
|
|
532
|
+
JSON.stringify(mappings.map(({ role, repository, repositoryRole, repositoryPath, path, sourcePath }) => ({ role, repository, repositoryRole, repositoryPath, path, sourcePath }))),
|
|
533
|
+
JSON.stringify(adapterSnapshot),
|
|
534
|
+
JSON.stringify(limits),
|
|
535
|
+
JSON.stringify(repositoryRevisions),
|
|
536
|
+
createdAt,
|
|
537
|
+
createdAt,
|
|
538
|
+
);
|
|
539
|
+
const { config } = loadProjectConfig(projectRoot);
|
|
540
|
+
const request = {
|
|
541
|
+
schema: 'ewai.starter-source-request/v1',
|
|
542
|
+
attemptId,
|
|
543
|
+
nonce,
|
|
544
|
+
project: { name: safeName(config.project.name, 'Project name') },
|
|
545
|
+
receipt: {
|
|
546
|
+
id: receipt.id,
|
|
547
|
+
name: receipt.name,
|
|
548
|
+
source: receipt.source,
|
|
549
|
+
sourceClass: receipt.sourceClass,
|
|
550
|
+
version: receipt.version,
|
|
551
|
+
digest: receipt.digest,
|
|
552
|
+
licence: receipt.licence,
|
|
553
|
+
compatibility: receipt.compatibility,
|
|
554
|
+
targets: receipt.targets,
|
|
555
|
+
},
|
|
556
|
+
stagingRoot,
|
|
557
|
+
limits,
|
|
558
|
+
};
|
|
559
|
+
await runAdapter(adapter, request, limits);
|
|
560
|
+
if (!existsSync(stagingRoot) || lstatSync(stagingRoot).isSymbolicLink() || realpathSync(stagingRoot) !== stagingRoot) throw new Error('Starter staging root changed during adapter execution.');
|
|
561
|
+
const fullTree = canonicalStarterTree(stagingRoot, { limits });
|
|
562
|
+
const targetTrees = mappings.map((mapping) => {
|
|
563
|
+
const prefix = mapping.sourcePath === '.' ? '' : `${mapping.sourcePath}/`;
|
|
564
|
+
const files = fullTree.filter((file) => mapping.sourcePath === '.' || file.path.startsWith(prefix)).map((file) => ({
|
|
565
|
+
...file,
|
|
566
|
+
path: mapping.sourcePath === '.' ? file.path : file.path.slice(prefix.length),
|
|
567
|
+
}));
|
|
568
|
+
if (!files.length) throw new Error(`Starter adapter produced no files for target ${mapping.role}.`);
|
|
569
|
+
return { role: mapping.role, files, digest: starterTreeDigest(files) };
|
|
570
|
+
});
|
|
571
|
+
for (const file of fullTree) {
|
|
572
|
+
const owners = mappings.filter((mapping) => mapping.sourcePath === '.' || file.path.startsWith(`${mapping.sourcePath}/`));
|
|
573
|
+
if (owners.length !== 1) throw new Error(`Starter adapter produced content outside one declared target: ${file.path}`);
|
|
574
|
+
}
|
|
575
|
+
const treeDigest = starterPackDigest(targetTrees);
|
|
576
|
+
if (treeDigest !== receipt.digest) throw new Error('Independently verified starter tree digest does not match the accepted receipt.');
|
|
577
|
+
const materialisationFiles = targetTrees.flatMap((target) => {
|
|
578
|
+
const mapping = mappings.find((candidate) => candidate.role === target.role);
|
|
579
|
+
return target.files.map((file) => ({
|
|
580
|
+
target_role: target.role,
|
|
581
|
+
repository_name: mapping.repository,
|
|
582
|
+
source_path: file.path,
|
|
583
|
+
destination_path: relativeDestination(mapping, file.path),
|
|
584
|
+
content_digest: file.digest,
|
|
585
|
+
size_bytes: file.bytes,
|
|
586
|
+
}));
|
|
587
|
+
});
|
|
588
|
+
const classified = classifyDestinations(projectRoot, mappings, materialisationFiles);
|
|
589
|
+
const previewId = randomUUID();
|
|
590
|
+
const expiresAt = new Date(new Date(createdAt).getTime() + previewLifetimeMs).toISOString();
|
|
591
|
+
const previewDigest = sha256(JSON.stringify(previewBinding({
|
|
592
|
+
previewId,
|
|
593
|
+
attemptId,
|
|
594
|
+
receipt,
|
|
595
|
+
adapter: safeAdapter(row),
|
|
596
|
+
mappings,
|
|
597
|
+
repositoryRevisions,
|
|
598
|
+
treeDigest,
|
|
599
|
+
targetTreeDigests: targetTrees.map(({ role, digest }) => ({ role, digest })),
|
|
600
|
+
files: classified,
|
|
601
|
+
expiresAt,
|
|
602
|
+
})));
|
|
603
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
604
|
+
try {
|
|
605
|
+
for (const file of classified) {
|
|
606
|
+
database.prepare(`
|
|
607
|
+
INSERT INTO starter_files (
|
|
608
|
+
attempt_id, target_role, repository_name, source_path, destination_path,
|
|
609
|
+
content_digest, size_bytes, classification, created_at
|
|
610
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
611
|
+
`).run(
|
|
612
|
+
attemptId,
|
|
613
|
+
file.target_role,
|
|
614
|
+
file.repository_name,
|
|
615
|
+
file.source_path,
|
|
616
|
+
file.destination_path,
|
|
617
|
+
file.content_digest,
|
|
618
|
+
file.size_bytes,
|
|
619
|
+
file.classification,
|
|
620
|
+
createdAt,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
database.prepare(`
|
|
624
|
+
UPDATE starter_attempts SET status = 'prepared', tree_digest = ?,
|
|
625
|
+
target_tree_digests_json = ?, preview_id = ?, preview_digest = ?,
|
|
626
|
+
expires_at = ?, updated_at = ? WHERE id = ?
|
|
627
|
+
`).run(
|
|
628
|
+
treeDigest,
|
|
629
|
+
JSON.stringify(targetTrees.map(({ role, digest }) => ({ role, digest }))),
|
|
630
|
+
previewId,
|
|
631
|
+
previewDigest,
|
|
632
|
+
expiresAt,
|
|
633
|
+
now(input),
|
|
634
|
+
attemptId,
|
|
635
|
+
);
|
|
636
|
+
database.exec('COMMIT;');
|
|
637
|
+
} catch (error) {
|
|
638
|
+
database.exec('ROLLBACK;');
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
return safePreparedProjection({
|
|
642
|
+
id: attemptId,
|
|
643
|
+
receiptId: receipt.id,
|
|
644
|
+
adapterId: adapter.id,
|
|
645
|
+
treeDigest,
|
|
646
|
+
repositoryRevisions,
|
|
647
|
+
previewId,
|
|
648
|
+
previewDigest,
|
|
649
|
+
expiresAt,
|
|
650
|
+
}, mappings, targetTrees, classified);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
const row = database.prepare('SELECT id FROM starter_attempts WHERE id = ?').get(attemptId);
|
|
653
|
+
if (row) database.prepare("UPDATE starter_attempts SET status = 'failed', error_code = 'preparation-failed', updated_at = ? WHERE id = ?").run(now(input), attemptId);
|
|
654
|
+
if (stagingRoot && existsSync(stagingRoot)) rmSync(stagingRoot, { recursive: true, force: true });
|
|
655
|
+
throw error;
|
|
656
|
+
} finally {
|
|
657
|
+
database.close();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function rebuildPreparedTree(projectRoot, attempt, mappings, files) {
|
|
662
|
+
if (!existsSync(attempt.staging_root) || lstatSync(attempt.staging_root).isSymbolicLink()) {
|
|
663
|
+
throw new Error('Prepared starter staging tree is missing or unsafe.');
|
|
664
|
+
}
|
|
665
|
+
const stagingRoot = realpathSync(attempt.staging_root);
|
|
666
|
+
if (stagingRoot !== attempt.staging_root) throw new Error('Prepared starter staging root has drifted.');
|
|
667
|
+
const limits = parseJson(attempt.limits_json, defaultLimits);
|
|
668
|
+
const fullTree = canonicalStarterTree(stagingRoot, { limits });
|
|
669
|
+
const targetTrees = mappings.map((mapping) => {
|
|
670
|
+
const prefix = mapping.sourcePath === '.' ? '' : `${mapping.sourcePath}/`;
|
|
671
|
+
const targetFiles = fullTree
|
|
672
|
+
.filter((file) => mapping.sourcePath === '.' || file.path.startsWith(prefix))
|
|
673
|
+
.map((file) => ({ ...file, path: mapping.sourcePath === '.' ? file.path : file.path.slice(prefix.length) }));
|
|
674
|
+
if (!targetFiles.length) throw new Error(`Prepared starter staging tree has no files for target ${mapping.role}.`);
|
|
675
|
+
return { role: mapping.role, files: targetFiles, digest: starterTreeDigest(targetFiles) };
|
|
676
|
+
});
|
|
677
|
+
for (const file of fullTree) {
|
|
678
|
+
const owners = mappings.filter((mapping) => mapping.sourcePath === '.' || file.path.startsWith(`${mapping.sourcePath}/`));
|
|
679
|
+
if (owners.length !== 1) throw new Error(`Prepared starter staging tree ownership has drifted: ${file.path}`);
|
|
680
|
+
}
|
|
681
|
+
const treeDigest = starterPackDigest(targetTrees);
|
|
682
|
+
if (treeDigest !== attempt.tree_digest) throw new Error('Prepared starter staging tree digest has drifted.');
|
|
683
|
+
if (!sameJson(targetTrees.map(({ role, digest }) => ({ role, digest })), parseJson(attempt.target_tree_digests_json, []))) {
|
|
684
|
+
throw new Error('Prepared starter target-tree digests have drifted.');
|
|
685
|
+
}
|
|
686
|
+
const staged = new Map();
|
|
687
|
+
for (const target of targetTrees) {
|
|
688
|
+
for (const file of target.files) staged.set(`${target.role}:${file.path}`, file);
|
|
689
|
+
}
|
|
690
|
+
if (staged.size !== files.length) throw new Error('Prepared starter staged-file inventory has drifted.');
|
|
691
|
+
for (const file of files) {
|
|
692
|
+
const current = staged.get(`${file.target_role}:${file.source_path}`);
|
|
693
|
+
if (!current || current.digest !== file.content_digest || current.bytes !== file.size_bytes) {
|
|
694
|
+
throw new Error(`Prepared starter staged file has drifted: ${file.destination_path}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return { targetTrees, staged };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function validateCurrentPreview(projectRoot, attempt, input, database) {
|
|
701
|
+
if (attempt.status !== 'prepared') throw new Error(`Starter materialisation preview is not available in prepared state: ${attempt.status}`);
|
|
702
|
+
if (new Date(now(input)).getTime() > new Date(attempt.expires_at).getTime()) throw new Error('Starter materialisation preview has expired and must be prepared again.');
|
|
703
|
+
const [receipt] = resolveAcceptedStarterReceipts(projectRoot, { ...input, receiptId: attempt.receipt_id });
|
|
704
|
+
if (!sameJson(receipt, parseJson(attempt.receipt_json, {}))) throw new Error('Accepted Governed Starter Pack receipt has drifted since preview.');
|
|
705
|
+
const mappings = resolveStarterTargetMappings(projectRoot, receipt);
|
|
706
|
+
if (!sameJson(snapshotMappings(mappings), parseJson(attempt.target_mappings_json, []))) {
|
|
707
|
+
throw new Error('Starter target mappings have drifted since preview.');
|
|
708
|
+
}
|
|
709
|
+
const adapterRow = database.prepare('SELECT * FROM starter_adapters WHERE id = ?').get(attempt.adapter_id);
|
|
710
|
+
if (!adapterRow) throw new Error(`Registered starter adapter disappeared: ${attempt.adapter_id}`);
|
|
711
|
+
revalidateAdapter(adapterRow);
|
|
712
|
+
const adapter = safeAdapter(adapterRow);
|
|
713
|
+
if (!sameJson(adapter, parseJson(attempt.adapter_snapshot_json, {}))) throw new Error('Registered starter adapter has drifted since preview.');
|
|
714
|
+
const repositoryRevisions = starterProjectRevision(mappings);
|
|
715
|
+
if (!sameJson(repositoryRevisions, parseJson(attempt.repository_revisions_json, {}))) {
|
|
716
|
+
throw new Error('Starter materialisation repository revision has drifted since preview.');
|
|
717
|
+
}
|
|
718
|
+
const storedFiles = database.prepare('SELECT * FROM starter_files WHERE attempt_id = ? ORDER BY target_role, source_path').all(attempt.id);
|
|
719
|
+
const { targetTrees, staged } = rebuildPreparedTree(projectRoot, attempt, mappings, storedFiles);
|
|
720
|
+
const classified = classifyDestinations(projectRoot, mappings, storedFiles);
|
|
721
|
+
for (const file of classified) {
|
|
722
|
+
const stored = storedFiles.find((candidate) => candidate.id === file.id);
|
|
723
|
+
if (file.classification !== stored.classification) throw new Error(`Starter destination classification has drifted: ${file.destination_path}`);
|
|
724
|
+
}
|
|
725
|
+
const digest = sha256(JSON.stringify(previewBinding({
|
|
726
|
+
previewId: attempt.preview_id,
|
|
727
|
+
attemptId: attempt.id,
|
|
728
|
+
receipt,
|
|
729
|
+
adapter,
|
|
730
|
+
mappings,
|
|
731
|
+
repositoryRevisions,
|
|
732
|
+
treeDigest: attempt.tree_digest,
|
|
733
|
+
targetTreeDigests: targetTrees.map(({ role, digest: targetDigest }) => ({ role, digest: targetDigest })),
|
|
734
|
+
files: classified,
|
|
735
|
+
expiresAt: attempt.expires_at,
|
|
736
|
+
})));
|
|
737
|
+
if (digest !== attempt.preview_digest) throw new Error('Starter materialisation preview binding has drifted.');
|
|
738
|
+
return { receipt, mappings, adapter, repositoryRevisions, files: classified, staged, targetTrees };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function persistJournal(database, attemptId, journal, input = {}) {
|
|
742
|
+
database.prepare('UPDATE starter_attempts SET journal_json = ?, updated_at = ? WHERE id = ?')
|
|
743
|
+
.run(JSON.stringify(journal), now(input), attemptId);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function syncDirectory(path) {
|
|
747
|
+
let descriptor;
|
|
748
|
+
try {
|
|
749
|
+
descriptor = openSync(path, constants.O_RDONLY);
|
|
750
|
+
fsyncSync(descriptor);
|
|
751
|
+
} catch {
|
|
752
|
+
// Directory fsync is not available on every supported filesystem.
|
|
753
|
+
} finally {
|
|
754
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function createParents(repositoryRoot, destinationPath, database, attemptId, journal, input) {
|
|
759
|
+
const segments = destinationPath.split('/').slice(0, -1);
|
|
760
|
+
let cursor = repositoryRoot;
|
|
761
|
+
for (const segment of segments) {
|
|
762
|
+
cursor = resolve(cursor, segment);
|
|
763
|
+
if (existsSync(cursor)) {
|
|
764
|
+
const metadata = lstatSync(cursor);
|
|
765
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) throw new Error(`Starter destination parent is not a real directory: ${destinationPath}`);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const entry = { kind: 'directory', state: 'creating', path: cursor, destinationPath: relative(repositoryRoot, cursor).replaceAll('\\', '/') };
|
|
769
|
+
journal.push(entry);
|
|
770
|
+
persistJournal(database, attemptId, journal, input);
|
|
771
|
+
mkdirSync(cursor, { recursive: false, mode: 0o755 });
|
|
772
|
+
entry.state = 'created';
|
|
773
|
+
persistJournal(database, attemptId, journal, input);
|
|
774
|
+
syncDirectory(resolve(cursor, '..'));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function publishExclusive(file, content, database, attemptId, journal, input, created) {
|
|
779
|
+
const parent = resolve(file.destination, '..');
|
|
780
|
+
const temporary = resolve(parent, `.ewai-starter-${randomUUID()}.tmp`);
|
|
781
|
+
const entry = {
|
|
782
|
+
kind: 'file',
|
|
783
|
+
state: 'publishing',
|
|
784
|
+
path: file.destination,
|
|
785
|
+
temporary,
|
|
786
|
+
destinationPath: file.destination_path,
|
|
787
|
+
digest: file.content_digest,
|
|
788
|
+
};
|
|
789
|
+
journal.push(entry);
|
|
790
|
+
persistJournal(database, attemptId, journal, input);
|
|
791
|
+
let descriptor;
|
|
792
|
+
try {
|
|
793
|
+
descriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
794
|
+
writeFileSync(descriptor, content);
|
|
795
|
+
fsyncSync(descriptor);
|
|
796
|
+
closeSync(descriptor);
|
|
797
|
+
descriptor = undefined;
|
|
798
|
+
chmodSync(temporary, 0o644);
|
|
799
|
+
input.testHooks?.beforePublish?.({ created });
|
|
800
|
+
linkSync(temporary, file.destination);
|
|
801
|
+
entry.state = 'created';
|
|
802
|
+
persistJournal(database, attemptId, journal, input);
|
|
803
|
+
unlinkSync(temporary);
|
|
804
|
+
syncDirectory(parent);
|
|
805
|
+
} catch (error) {
|
|
806
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
807
|
+
const raced = error?.code === 'EEXIST' ? new Error(`Starter destination appeared during exclusive publication: ${file.destination_path}`) : error;
|
|
808
|
+
throw raced;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function sameInode(left, right) {
|
|
813
|
+
if (!existsSync(left) || !existsSync(right)) return false;
|
|
814
|
+
const leftMetadata = lstatSync(left);
|
|
815
|
+
const rightMetadata = lstatSync(right);
|
|
816
|
+
return leftMetadata.isFile() && rightMetadata.isFile() && leftMetadata.dev === rightMetadata.dev && leftMetadata.ino === rightMetadata.ino;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function rollbackJournal(journal) {
|
|
820
|
+
const preserved = [];
|
|
821
|
+
for (const entry of [...journal].reverse()) {
|
|
822
|
+
if (entry.kind === 'file') {
|
|
823
|
+
const ownsPublished = entry.state === 'created' || (entry.state === 'publishing' && sameInode(entry.path, entry.temporary));
|
|
824
|
+
if (ownsPublished && existsSync(entry.path)) {
|
|
825
|
+
if (regularFileDigest(entry.path) === entry.digest) unlinkSync(entry.path);
|
|
826
|
+
else preserved.push(entry.destinationPath);
|
|
827
|
+
}
|
|
828
|
+
if (existsSync(entry.temporary) && regularFileDigest(entry.temporary) === entry.digest) unlinkSync(entry.temporary);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (entry.kind === 'directory' && entry.state === 'created' && existsSync(entry.path)) {
|
|
832
|
+
const metadata = lstatSync(entry.path);
|
|
833
|
+
if (!metadata.isSymbolicLink() && metadata.isDirectory() && readdirSync(entry.path).length === 0) rmdirSync(entry.path);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return [...new Set(preserved)].sort();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function writeExclusiveEvidence(path, content) {
|
|
840
|
+
const folder = resolve(path, '..');
|
|
841
|
+
mkdirSync(folder, { recursive: true, mode: 0o755 });
|
|
842
|
+
const temporary = resolve(folder, `.ewai-evidence-${randomUUID()}.tmp`);
|
|
843
|
+
let descriptor;
|
|
844
|
+
try {
|
|
845
|
+
descriptor = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
846
|
+
writeFileSync(descriptor, content);
|
|
847
|
+
fsyncSync(descriptor);
|
|
848
|
+
closeSync(descriptor);
|
|
849
|
+
descriptor = undefined;
|
|
850
|
+
chmodSync(temporary, 0o644);
|
|
851
|
+
linkSync(temporary, path);
|
|
852
|
+
unlinkSync(temporary);
|
|
853
|
+
syncDirectory(folder);
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
856
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function materialisationEvidence(projectRoot, attempt, context, approval, personas, completedAt) {
|
|
862
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
863
|
+
const evidenceFolder = resolve(paths.specsRoot, '3.Evidence/starter-materialisations');
|
|
864
|
+
const jsonPath = resolve(evidenceFolder, `${attempt.id}.json`);
|
|
865
|
+
const markdownPath = resolve(evidenceFolder, `${attempt.id}.md`);
|
|
866
|
+
const relativeJson = relative(resolve(projectRoot), jsonPath).replaceAll('\\', '/');
|
|
867
|
+
const relativeMarkdown = relative(resolve(projectRoot), markdownPath).replaceAll('\\', '/');
|
|
868
|
+
const counts = classificationCounts(context.files);
|
|
869
|
+
const evidence = {
|
|
870
|
+
schema: 'ewai.starter-materialisation-evidence/v1',
|
|
871
|
+
attemptId: attempt.id,
|
|
872
|
+
previewId: attempt.preview_id,
|
|
873
|
+
previewDigest: attempt.preview_digest,
|
|
874
|
+
receiptId: attempt.receipt_id,
|
|
875
|
+
adapterId: attempt.adapter_id,
|
|
876
|
+
treeDigest: attempt.tree_digest,
|
|
877
|
+
approval: { approvedBy: approval.approvedBy, approvedAt: approval.approvedAt },
|
|
878
|
+
result: { created: counts.create, identical: counts.identical, completedAt },
|
|
879
|
+
repositories: context.repositoryRevisions.repositories.map(({ name, revision }) => ({ name, revision })),
|
|
880
|
+
targets: context.targetTrees.map(({ role, digest }) => ({ role, digest })),
|
|
881
|
+
destinations: context.files.map((file) => ({
|
|
882
|
+
targetRole: file.target_role,
|
|
883
|
+
repository: file.repository_name,
|
|
884
|
+
path: file.destination_path,
|
|
885
|
+
digest: file.content_digest,
|
|
886
|
+
classification: file.classification,
|
|
887
|
+
})),
|
|
888
|
+
personas,
|
|
889
|
+
notice: STARTER_MATERIALISATION_NOTICE,
|
|
890
|
+
};
|
|
891
|
+
const markdown = `# Governed Starter Pack materialisation\n\n- Attempt: \`${attempt.id}\`\n- Preview: \`${attempt.preview_id}\`\n- Receipt: \`${attempt.receipt_id}\`\n- Adapter: \`${attempt.adapter_id}\`\n- Tree digest: \`${attempt.tree_digest}\`\n- Approved by: ${approval.approvedBy}\n- Approved at: ${approval.approvedAt}\n- Completed at: ${completedAt}\n- Created: ${counts.create}\n- Already identical: ${counts.identical}\n\n${STARTER_MATERIALISATION_NOTICE}\n`;
|
|
892
|
+
writeExclusiveEvidence(jsonPath, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
893
|
+
try {
|
|
894
|
+
writeExclusiveEvidence(markdownPath, markdown);
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (existsSync(jsonPath) && sha256(readFileSync(jsonPath)) === sha256(`${JSON.stringify(evidence, null, 2)}\n`)) unlinkSync(jsonPath);
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
return { jsonPath, markdownPath, relativeJson, relativeMarkdown, evidence, counts };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export function applyStarterMaterialisation(projectRoot, previewId, input = {}) {
|
|
903
|
+
if (input.confirmed !== true) throw new Error('Starter materialisation application requires explicit confirmation.');
|
|
904
|
+
const approvedBy = safeName(input.approvedBy, 'Starter materialisation approver', 160);
|
|
905
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
906
|
+
let applying = false;
|
|
907
|
+
let journal = [];
|
|
908
|
+
let writtenEvidence = null;
|
|
909
|
+
try {
|
|
910
|
+
const attempt = database.prepare('SELECT * FROM starter_attempts WHERE preview_id = ?').get(String(previewId ?? '').trim());
|
|
911
|
+
if (!attempt) throw new Error(`Unknown starter materialisation preview: ${previewId}`);
|
|
912
|
+
const context = validateCurrentPreview(projectRoot, attempt, input, database);
|
|
913
|
+
const counts = classificationCounts(context.files);
|
|
914
|
+
if (counts.conflict) throw new Error(`Starter materialisation preview contains ${counts.conflict} destination conflict(s).`);
|
|
915
|
+
const approvedAt = now(input);
|
|
916
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
917
|
+
try {
|
|
918
|
+
database.prepare(`
|
|
919
|
+
INSERT INTO starter_approvals (id, attempt_id, preview_id, approved_by, approved_at, created_at)
|
|
920
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
921
|
+
`).run(randomUUID(), attempt.id, attempt.preview_id, approvedBy, approvedAt, approvedAt);
|
|
922
|
+
database.prepare("UPDATE starter_attempts SET status = 'applying', updated_at = ? WHERE id = ? AND status = 'prepared'")
|
|
923
|
+
.run(approvedAt, attempt.id);
|
|
924
|
+
database.exec('COMMIT;');
|
|
925
|
+
} catch (error) {
|
|
926
|
+
database.exec('ROLLBACK;');
|
|
927
|
+
throw error;
|
|
928
|
+
}
|
|
929
|
+
applying = true;
|
|
930
|
+
let created = 0;
|
|
931
|
+
for (const file of context.files) {
|
|
932
|
+
if (file.classification === 'identical') continue;
|
|
933
|
+
const mapping = context.mappings.find((candidate) => candidate.role === file.target_role);
|
|
934
|
+
createParents(mapping.repositoryRoot, file.destination_path, database, attempt.id, journal, input);
|
|
935
|
+
const staged = context.staged.get(`${file.target_role}:${file.source_path}`);
|
|
936
|
+
publishExclusive(file, staged.content, database, attempt.id, journal, input, created);
|
|
937
|
+
created += 1;
|
|
938
|
+
input.testHooks?.afterPublish?.({ created });
|
|
939
|
+
}
|
|
940
|
+
const completedAt = now(input);
|
|
941
|
+
const personas = resolveLifecyclePersonas(projectRoot, input.personas ?? []);
|
|
942
|
+
writtenEvidence = materialisationEvidence(
|
|
943
|
+
projectRoot,
|
|
944
|
+
attempt,
|
|
945
|
+
context,
|
|
946
|
+
{ approvedBy, approvedAt },
|
|
947
|
+
personas,
|
|
948
|
+
completedAt,
|
|
949
|
+
);
|
|
950
|
+
database.prepare(`
|
|
951
|
+
UPDATE starter_attempts SET status = 'completed', evidence_path = ?,
|
|
952
|
+
updated_at = ?, completed_at = ?, error_code = '' WHERE id = ?
|
|
953
|
+
`).run(writtenEvidence.relativeJson, completedAt, completedAt, attempt.id);
|
|
954
|
+
if (existsSync(attempt.staging_root)) rmSync(attempt.staging_root, { recursive: true, force: true });
|
|
955
|
+
publishLifecycleEventSafely(projectRoot, 'ewai.project.starter.materialised', {
|
|
956
|
+
sourceKey: `starter-materialisation:${attempt.id}`,
|
|
957
|
+
sourceRevision: attempt.preview_digest,
|
|
958
|
+
occurredAt: completedAt,
|
|
959
|
+
streamId: 'project',
|
|
960
|
+
facts: {
|
|
961
|
+
receiptId: attempt.receipt_id,
|
|
962
|
+
adapterId: attempt.adapter_id,
|
|
963
|
+
treeDigest: attempt.tree_digest,
|
|
964
|
+
targetCount: context.targetTrees.length,
|
|
965
|
+
repositoryCount: context.repositoryRevisions.repositories.length,
|
|
966
|
+
createdCount: writtenEvidence.counts.create,
|
|
967
|
+
identicalCount: writtenEvidence.counts.identical,
|
|
968
|
+
completedAt,
|
|
969
|
+
},
|
|
970
|
+
evidence: [writtenEvidence.relativeJson, writtenEvidence.relativeMarkdown],
|
|
971
|
+
personas: input.personas ?? [],
|
|
972
|
+
});
|
|
973
|
+
return {
|
|
974
|
+
schema: 'ewai.starter-materialisation-result/v1',
|
|
975
|
+
attemptId: attempt.id,
|
|
976
|
+
previewId: attempt.preview_id,
|
|
977
|
+
status: 'completed',
|
|
978
|
+
results: { created: writtenEvidence.counts.create, identical: writtenEvidence.counts.identical },
|
|
979
|
+
evidence: { json: writtenEvidence.relativeJson, markdown: writtenEvidence.relativeMarkdown },
|
|
980
|
+
personas,
|
|
981
|
+
notice: STARTER_MATERIALISATION_NOTICE,
|
|
982
|
+
};
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (applying) {
|
|
985
|
+
if (writtenEvidence) {
|
|
986
|
+
for (const path of [writtenEvidence.jsonPath, writtenEvidence.markdownPath]) {
|
|
987
|
+
if (existsSync(path)) unlinkSync(path);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const preserved = rollbackJournal(journal);
|
|
991
|
+
const status = preserved.length ? 'recovery-required' : 'failed';
|
|
992
|
+
const row = database.prepare("SELECT id FROM starter_attempts WHERE preview_id = ?").get(String(previewId ?? '').trim());
|
|
993
|
+
if (row) database.prepare('UPDATE starter_attempts SET status = ?, error_code = ?, journal_json = ?, updated_at = ? WHERE id = ?')
|
|
994
|
+
.run(status, status === 'recovery-required' ? 'digest-changed' : 'application-failed', JSON.stringify(journal), now(input), row.id);
|
|
995
|
+
if (preserved.length) throw new Error(`${error.message} Recovery required because created content changed: ${preserved.join(', ')}`);
|
|
996
|
+
}
|
|
997
|
+
throw error;
|
|
998
|
+
} finally {
|
|
999
|
+
database.close();
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export function recoverStarterMaterialisation(projectRoot, attemptId, input = {}) {
|
|
1004
|
+
if (input.confirmed !== true) throw new Error('Starter materialisation recovery requires explicit confirmation.');
|
|
1005
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
1006
|
+
try {
|
|
1007
|
+
const attempt = database.prepare('SELECT * FROM starter_attempts WHERE id = ?').get(String(attemptId ?? '').trim());
|
|
1008
|
+
if (!attempt) throw new Error(`Unknown starter materialisation attempt: ${attemptId}`);
|
|
1009
|
+
if (!['applying', 'recovery-required', 'failed'].includes(attempt.status)) {
|
|
1010
|
+
throw new Error(`Starter materialisation attempt cannot be recovered from state ${attempt.status}.`);
|
|
1011
|
+
}
|
|
1012
|
+
const journal = parseJson(attempt.journal_json, []);
|
|
1013
|
+
const preserved = rollbackJournal(journal);
|
|
1014
|
+
const status = preserved.length ? 'recovery-required' : 'failed';
|
|
1015
|
+
database.prepare('UPDATE starter_attempts SET status = ?, error_code = ?, updated_at = ? WHERE id = ?')
|
|
1016
|
+
.run(status, preserved.length ? 'digest-changed' : 'recovered', now(input), attempt.id);
|
|
1017
|
+
return {
|
|
1018
|
+
schema: 'ewai.starter-materialisation-recovery/v1',
|
|
1019
|
+
attemptId: attempt.id,
|
|
1020
|
+
status,
|
|
1021
|
+
preserved,
|
|
1022
|
+
notice: 'Recovery removes only EWAI-created files whose content digest is unchanged. Changed files are preserved for human resolution.',
|
|
1023
|
+
};
|
|
1024
|
+
} finally {
|
|
1025
|
+
database.close();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function safeReceiptProjection(receipt) {
|
|
1030
|
+
return {
|
|
1031
|
+
id: receipt.id,
|
|
1032
|
+
name: receipt.name,
|
|
1033
|
+
sourceClass: receipt.sourceClass,
|
|
1034
|
+
version: receipt.version,
|
|
1035
|
+
digest: receipt.digest,
|
|
1036
|
+
licence: receipt.licence,
|
|
1037
|
+
compatibility: receipt.compatibility,
|
|
1038
|
+
targets: receipt.targets.map(({ role }) => ({ role })),
|
|
1039
|
+
pack: receipt.pack,
|
|
1040
|
+
module: receipt.module,
|
|
1041
|
+
accepted: receipt.accepted,
|
|
1042
|
+
...(receipt.legacy ? { legacy: true } : {}),
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export function selectStarterPersonas(personas, moment = 'review') {
|
|
1047
|
+
const catalogue = Array.isArray(personas) ? personas : [];
|
|
1048
|
+
const byId = new Map(catalogue.map((persona) => [persona.id, persona]));
|
|
1049
|
+
const selected = [];
|
|
1050
|
+
const add = (persona, role, reason) => {
|
|
1051
|
+
if (!persona || selected.some(({ ref }) => ref === persona.id)) return;
|
|
1052
|
+
selected.push({ ref: persona.id, name: persona.name, tier: persona.tier, role, reason });
|
|
1053
|
+
};
|
|
1054
|
+
const productSignal = (persona) => /product|owner|delivery|platform|architect/i.test([
|
|
1055
|
+
persona.id,
|
|
1056
|
+
persona.name,
|
|
1057
|
+
persona.category,
|
|
1058
|
+
...(persona.tags ?? []),
|
|
1059
|
+
].join(' '));
|
|
1060
|
+
const projectPersona = catalogue.find((persona) => persona.tier === 'project' && productSignal(persona));
|
|
1061
|
+
const premiumPersonas = catalogue.filter((persona) => persona.tier === 'premium' && productSignal(persona)).slice(0, 2);
|
|
1062
|
+
const reasons = {
|
|
1063
|
+
review: 'Reviewing intended outcomes, topology, compatibility and the immutable create/identical/conflict summary.',
|
|
1064
|
+
apply: 'Challenging operational readiness before named human approval and additive application.',
|
|
1065
|
+
recovery: 'Assessing safe recovery while preserving any generated content that has changed.',
|
|
1066
|
+
};
|
|
1067
|
+
const reason = reasons[moment] ?? reasons.review;
|
|
1068
|
+
if (moment !== 'recovery') {
|
|
1069
|
+
add(projectPersona, 'consulted', `${reason} Project-local organisational context is active.`);
|
|
1070
|
+
for (const persona of premiumPersonas) add(persona, 'consulted', `${reason} Installed premium expertise is active.`);
|
|
1071
|
+
}
|
|
1072
|
+
const coreIds = moment === 'recovery'
|
|
1073
|
+
? ['ewai.core.operator', 'ewai.core.maintainer']
|
|
1074
|
+
: moment === 'apply'
|
|
1075
|
+
? ['ewai.core.operator', 'ewai.core.maintainer', 'ewai.core.end-user']
|
|
1076
|
+
: ['ewai.core.operator', 'ewai.core.end-user', 'ewai.core.specs-knowledge-curator', 'ewai.core.maintainer'];
|
|
1077
|
+
for (const [index, id] of coreIds.entries()) add(byId.get(id), index === 0 ? 'primary' : 'consulted', reason);
|
|
1078
|
+
return selected.slice(0, 8);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function safeAttemptProjection(database, attempt) {
|
|
1082
|
+
const files = database.prepare(`
|
|
1083
|
+
SELECT target_role, repository_name, classification, COUNT(*) AS files, SUM(size_bytes) AS bytes
|
|
1084
|
+
FROM starter_files WHERE attempt_id = ?
|
|
1085
|
+
GROUP BY target_role, repository_name, classification
|
|
1086
|
+
ORDER BY target_role, repository_name, classification
|
|
1087
|
+
`).all(attempt.id);
|
|
1088
|
+
const mappings = parseJson(attempt.target_mappings_json, []);
|
|
1089
|
+
const targets = mappings.map((mapping) => {
|
|
1090
|
+
const targetFiles = files.filter((file) => file.target_role === mapping.role);
|
|
1091
|
+
const classifications = { create: 0, identical: 0, conflict: 0 };
|
|
1092
|
+
for (const file of targetFiles) classifications[file.classification] = Number(file.files);
|
|
1093
|
+
return {
|
|
1094
|
+
role: mapping.role,
|
|
1095
|
+
repository: mapping.repository,
|
|
1096
|
+
path: mapping.path,
|
|
1097
|
+
files: targetFiles.reduce((total, file) => total + Number(file.files), 0),
|
|
1098
|
+
bytes: targetFiles.reduce((total, file) => total + Number(file.bytes), 0),
|
|
1099
|
+
classifications,
|
|
1100
|
+
};
|
|
1101
|
+
});
|
|
1102
|
+
const approval = database.prepare(`
|
|
1103
|
+
SELECT approved_by, approved_at, evidence_path FROM starter_approvals WHERE attempt_id = ?
|
|
1104
|
+
`).get(attempt.id);
|
|
1105
|
+
return {
|
|
1106
|
+
attemptId: attempt.id,
|
|
1107
|
+
receiptId: attempt.receipt_id,
|
|
1108
|
+
adapterId: attempt.adapter_id,
|
|
1109
|
+
status: attempt.status,
|
|
1110
|
+
treeDigest: attempt.tree_digest,
|
|
1111
|
+
previewId: attempt.preview_id,
|
|
1112
|
+
previewDigest: attempt.preview_digest,
|
|
1113
|
+
expiresAt: attempt.expires_at,
|
|
1114
|
+
targets,
|
|
1115
|
+
classifications: targets.reduce((counts, target) => {
|
|
1116
|
+
for (const classification of Object.keys(counts)) counts[classification] += target.classifications[classification];
|
|
1117
|
+
return counts;
|
|
1118
|
+
}, { create: 0, identical: 0, conflict: 0 }),
|
|
1119
|
+
...(approval ? { approval: { approvedBy: approval.approved_by, approvedAt: approval.approved_at } } : {}),
|
|
1120
|
+
...(attempt.evidence_path ? { evidence: attempt.evidence_path } : {}),
|
|
1121
|
+
errorCode: attempt.error_code || '',
|
|
1122
|
+
createdAt: attempt.created_at,
|
|
1123
|
+
updatedAt: attempt.updated_at,
|
|
1124
|
+
completedAt: attempt.completed_at,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
export function readStarterMaterialisationWorkspace(projectRoot, options = {}) {
|
|
1129
|
+
const limit = Number(options.limit ?? 50);
|
|
1130
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new Error('Starter materialisation workspace limit must be between 1 and 200.');
|
|
1131
|
+
let receipts = [];
|
|
1132
|
+
let receiptStatus = 'available';
|
|
1133
|
+
try {
|
|
1134
|
+
receipts = resolveAcceptedStarterReceipts(projectRoot, options).map(safeReceiptProjection);
|
|
1135
|
+
} catch {
|
|
1136
|
+
receiptStatus = 'unavailable';
|
|
1137
|
+
}
|
|
1138
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
1139
|
+
try {
|
|
1140
|
+
const attempts = database.prepare('SELECT * FROM starter_attempts ORDER BY created_at DESC LIMIT ?').all(limit)
|
|
1141
|
+
.map((attempt) => safeAttemptProjection(database, attempt));
|
|
1142
|
+
return {
|
|
1143
|
+
schema: 'ewai.starter-materialisation-workspace/v1',
|
|
1144
|
+
receipts,
|
|
1145
|
+
receiptStatus,
|
|
1146
|
+
adapters: database.prepare('SELECT * FROM starter_adapters ORDER BY id').all().map(safeAdapter),
|
|
1147
|
+
attempts,
|
|
1148
|
+
activePersonas: resolveLifecyclePersonas(projectRoot, options.personas ?? []),
|
|
1149
|
+
notice: STARTER_MATERIALISATION_NOTICE,
|
|
1150
|
+
adapterNotice: STARTER_ADAPTER_TRUST_NOTICE,
|
|
1151
|
+
};
|
|
1152
|
+
} finally {
|
|
1153
|
+
database.close();
|
|
1154
|
+
}
|
|
1155
|
+
}
|