@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,299 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { deriveExecutionState } from '../execution-state.mjs';
|
|
5
|
+
import { dependencyRequirementBlockers } from '../intent-dependencies.mjs';
|
|
6
|
+
import { validateTaskReport } from '../task-graph.mjs';
|
|
7
|
+
import { projectPaths } from '../paths.mjs';
|
|
8
|
+
import { openRuntimeDatabase } from './database.mjs';
|
|
9
|
+
import { syncWorkItems } from './work.mjs';
|
|
10
|
+
|
|
11
|
+
const defaultDurationMs = 15 * 60 * 1000;
|
|
12
|
+
const maximumDurationMs = 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function required(value, label) {
|
|
15
|
+
const normalized = String(value ?? '').trim();
|
|
16
|
+
if (!normalized) throw new Error(`Execution lease requires ${label}.`);
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function duration(value) {
|
|
21
|
+
const milliseconds = Number(value ?? defaultDurationMs);
|
|
22
|
+
if (!Number.isInteger(milliseconds) || milliseconds < 1_000 || milliseconds > maximumDurationMs) {
|
|
23
|
+
throw new Error(`Execution lease duration must be between 1000 and ${maximumDurationMs} milliseconds.`);
|
|
24
|
+
}
|
|
25
|
+
return milliseconds;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function timestamp(value = new Date()) {
|
|
29
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rowToLease(row) {
|
|
33
|
+
return {
|
|
34
|
+
id: row.id,
|
|
35
|
+
workItemId: row.work_item_id,
|
|
36
|
+
taskId: row.task_id,
|
|
37
|
+
ownerId: row.owner_id,
|
|
38
|
+
tool: row.tool,
|
|
39
|
+
runId: row.run_id,
|
|
40
|
+
token: row.token,
|
|
41
|
+
status: row.status,
|
|
42
|
+
acquiredAt: row.acquired_at,
|
|
43
|
+
heartbeatAt: row.heartbeat_at,
|
|
44
|
+
expiresAt: row.expires_at,
|
|
45
|
+
releasedAt: row.released_at,
|
|
46
|
+
outcome: row.outcome,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rowToIntent(row) {
|
|
51
|
+
return {
|
|
52
|
+
id: row.id,
|
|
53
|
+
slug: row.slug,
|
|
54
|
+
domain: row.domain,
|
|
55
|
+
title: row.title,
|
|
56
|
+
status: row.status,
|
|
57
|
+
deliveryStatus: row.delivery_status,
|
|
58
|
+
currentPhase: row.current_phase,
|
|
59
|
+
deliveryStatePath: row.delivery_state_path,
|
|
60
|
+
path: row.path,
|
|
61
|
+
relationships: JSON.parse(row.relationships_json || '[]'),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function findWorkItem(database, reference) {
|
|
66
|
+
const direct = database.prepare('SELECT * FROM work_items WHERE id = ?').get(reference);
|
|
67
|
+
if (direct) return direct;
|
|
68
|
+
const matches = database.prepare('SELECT * FROM work_items WHERE slug = ? ORDER BY id LIMIT 2').all(reference);
|
|
69
|
+
if (matches.length > 1) throw new Error(`Ambiguous work item slug: ${reference}`);
|
|
70
|
+
if (!matches.length) throw new Error(`Unknown work item: ${reference}`);
|
|
71
|
+
return matches[0];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function expireStale(database, at) {
|
|
75
|
+
database.prepare(`
|
|
76
|
+
UPDATE execution_leases
|
|
77
|
+
SET status = 'expired', released_at = ?, outcome = CASE WHEN outcome = '' THEN 'lease-expired' ELSE outcome END
|
|
78
|
+
WHERE status = 'active' AND expires_at <= ?
|
|
79
|
+
`).run(at, at);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function executionReadiness(projectRoot, database, item, taskId) {
|
|
83
|
+
const intentCatalog = new Map(database.prepare('SELECT * FROM intents ORDER BY id').all().map((row) => {
|
|
84
|
+
const intent = rowToIntent(row);
|
|
85
|
+
return [intent.id, intent];
|
|
86
|
+
}));
|
|
87
|
+
const intent = intentCatalog.get(item.intent_id);
|
|
88
|
+
if (!intent) throw new Error(`Cannot lease ${taskId}: intent projection ${item.intent_id} does not exist.`);
|
|
89
|
+
const leases = database.prepare(`
|
|
90
|
+
SELECT id, work_item_id, task_id, owner_id, tool, run_id, status,
|
|
91
|
+
acquired_at, heartbeat_at, expires_at, released_at, outcome
|
|
92
|
+
FROM execution_leases WHERE work_item_id = ? AND status = 'active' AND expires_at > ?
|
|
93
|
+
`).all(item.id, new Date().toISOString()).map((lease) => ({
|
|
94
|
+
id: lease.id,
|
|
95
|
+
workItemId: lease.work_item_id,
|
|
96
|
+
taskId: lease.task_id,
|
|
97
|
+
ownerId: lease.owner_id,
|
|
98
|
+
tool: lease.tool,
|
|
99
|
+
runId: lease.run_id,
|
|
100
|
+
status: lease.status,
|
|
101
|
+
acquiredAt: lease.acquired_at,
|
|
102
|
+
heartbeatAt: lease.heartbeat_at,
|
|
103
|
+
expiresAt: lease.expires_at,
|
|
104
|
+
releasedAt: lease.released_at,
|
|
105
|
+
outcome: lease.outcome,
|
|
106
|
+
}));
|
|
107
|
+
const execution = deriveExecutionState(projectRoot, intent, { leases, intentCatalog });
|
|
108
|
+
if (!execution.valid) {
|
|
109
|
+
const reasons = execution.blockers.map((blocker) => blocker.message).join('; ');
|
|
110
|
+
throw new Error(`Cannot lease ${taskId}: delivery evidence is invalid or inconsistent${reasons ? `: ${reasons}` : '.'}`);
|
|
111
|
+
}
|
|
112
|
+
const dependencyBlockers = dependencyRequirementBlockers(projectRoot, intent, intentCatalog, 'build');
|
|
113
|
+
if (dependencyBlockers.length) {
|
|
114
|
+
throw new Error(`Cannot lease ${taskId}: ${dependencyBlockers.map((blocker) => blocker.message).join('; ')}`);
|
|
115
|
+
}
|
|
116
|
+
const task = execution.tasks.tasks.find((candidate) => candidate.id === taskId);
|
|
117
|
+
if (!task) throw new Error(`Cannot lease ${taskId}: task does not exist in task-graph.json.`);
|
|
118
|
+
return { execution, task };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function withDatabase(projectRoot, operation) {
|
|
122
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
123
|
+
try {
|
|
124
|
+
syncWorkItems(projectRoot, database);
|
|
125
|
+
return operation(database);
|
|
126
|
+
} finally {
|
|
127
|
+
database.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function acquireExecutionLease(projectRoot, reference, input = {}) {
|
|
132
|
+
const taskId = required(input.taskId, 'taskId');
|
|
133
|
+
const ownerId = required(input.ownerId, 'ownerId');
|
|
134
|
+
const tool = String(input.tool ?? '').trim();
|
|
135
|
+
const runId = required(input.runId, 'runId');
|
|
136
|
+
const at = timestamp(input.now);
|
|
137
|
+
const expiresAt = timestamp(new Date(new Date(at).getTime() + duration(input.durationMs)));
|
|
138
|
+
|
|
139
|
+
return withDatabase(projectRoot, (database) => {
|
|
140
|
+
const item = findWorkItem(database, reference);
|
|
141
|
+
const readiness = executionReadiness(projectRoot, database, item, taskId);
|
|
142
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
143
|
+
try {
|
|
144
|
+
expireStale(database, at);
|
|
145
|
+
const existing = database.prepare(`
|
|
146
|
+
SELECT * FROM execution_leases WHERE work_item_id = ? AND task_id = ? AND status = 'active'
|
|
147
|
+
`).get(item.id, taskId);
|
|
148
|
+
if (existing) {
|
|
149
|
+
if (existing.owner_id !== ownerId || existing.run_id !== runId || existing.tool !== tool) {
|
|
150
|
+
throw new Error(`${taskId} is already leased by ${existing.owner_id} (${existing.tool || 'unknown tool'}).`);
|
|
151
|
+
}
|
|
152
|
+
database.prepare(`
|
|
153
|
+
UPDATE execution_leases SET heartbeat_at = ?, expires_at = ? WHERE id = ?
|
|
154
|
+
`).run(at, expiresAt, existing.id);
|
|
155
|
+
const renewed = database.prepare('SELECT * FROM execution_leases WHERE id = ?').get(existing.id);
|
|
156
|
+
database.exec('COMMIT;');
|
|
157
|
+
return rowToLease(renewed);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!readiness.execution.actions.acquireTask.permitted || readiness.task.status !== 'ready') {
|
|
161
|
+
const reason = readiness.execution.actions.acquireTask.blockers[0]?.message ?? `task state is ${readiness.task.status}`;
|
|
162
|
+
throw new Error(`Cannot lease ${taskId}: ${reason}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const activeCount = database.prepare(`
|
|
166
|
+
SELECT COUNT(*) AS count FROM execution_leases WHERE work_item_id = ? AND status = 'active'
|
|
167
|
+
`).get(item.id).count;
|
|
168
|
+
if (activeCount >= readiness.execution.tasks.maxParallelTasks) {
|
|
169
|
+
throw new Error(`Cannot lease ${taskId}: the task graph allows at most ${readiness.execution.tasks.maxParallelTasks} parallel task(s).`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const id = randomUUID();
|
|
173
|
+
const token = randomUUID();
|
|
174
|
+
database.prepare(`
|
|
175
|
+
INSERT INTO execution_leases (
|
|
176
|
+
id, work_item_id, task_id, owner_id, tool, run_id, token,
|
|
177
|
+
status, acquired_at, heartbeat_at, expires_at
|
|
178
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)
|
|
179
|
+
`).run(id, item.id, taskId, ownerId, tool, runId, token, at, at, expiresAt);
|
|
180
|
+
const created = database.prepare('SELECT * FROM execution_leases WHERE id = ?').get(id);
|
|
181
|
+
database.exec('COMMIT;');
|
|
182
|
+
return rowToLease(created);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
database.exec('ROLLBACK;');
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function heartbeatExecutionLease(projectRoot, leaseId, input = {}) {
|
|
191
|
+
const token = required(input.token, 'token');
|
|
192
|
+
const at = timestamp(input.now);
|
|
193
|
+
const expiresAt = timestamp(new Date(new Date(at).getTime() + duration(input.durationMs)));
|
|
194
|
+
return withDatabase(projectRoot, (database) => {
|
|
195
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
196
|
+
try {
|
|
197
|
+
expireStale(database, at);
|
|
198
|
+
const lease = database.prepare('SELECT * FROM execution_leases WHERE id = ?').get(leaseId);
|
|
199
|
+
if (!lease) throw new Error(`Unknown execution lease: ${leaseId}`);
|
|
200
|
+
if (lease.token !== token) throw new Error('Execution lease token does not match.');
|
|
201
|
+
if (lease.status !== 'active') throw new Error(`Execution lease is ${lease.status}, not active.`);
|
|
202
|
+
database.prepare('UPDATE execution_leases SET heartbeat_at = ?, expires_at = ? WHERE id = ?')
|
|
203
|
+
.run(at, expiresAt, leaseId);
|
|
204
|
+
const updated = database.prepare('SELECT * FROM execution_leases WHERE id = ?').get(leaseId);
|
|
205
|
+
database.exec('COMMIT;');
|
|
206
|
+
return rowToLease(updated);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
database.exec('ROLLBACK;');
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function releaseExecutionLease(projectRoot, leaseId, input = {}) {
|
|
215
|
+
const token = required(input.token, 'token');
|
|
216
|
+
const outcome = String(input.outcome ?? 'released').trim();
|
|
217
|
+
const at = timestamp(input.now);
|
|
218
|
+
return withDatabase(projectRoot, (database) => {
|
|
219
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
220
|
+
try {
|
|
221
|
+
expireStale(database, at);
|
|
222
|
+
const lease = database.prepare('SELECT * FROM execution_leases WHERE id = ?').get(leaseId);
|
|
223
|
+
if (!lease) throw new Error(`Unknown execution lease: ${leaseId}`);
|
|
224
|
+
if (lease.token !== token) throw new Error('Execution lease token does not match.');
|
|
225
|
+
if (lease.status !== 'active') throw new Error(`Execution lease is ${lease.status}, not active.`);
|
|
226
|
+
if (outcome === 'completed') {
|
|
227
|
+
const item = database.prepare('SELECT slug FROM work_items WHERE id = ?').get(lease.work_item_id);
|
|
228
|
+
const deliveryRoot = resolve(projectPaths(projectRoot).buildRoot, item.slug);
|
|
229
|
+
const graph = JSON.parse(readFileSync(resolve(deliveryRoot, 'task-graph.json'), 'utf8'));
|
|
230
|
+
const task = graph.tasks?.find((candidate) => candidate.id === lease.task_id);
|
|
231
|
+
if (!task) throw new Error(`Cannot complete ${lease.task_id}: task is missing from task-graph.json.`);
|
|
232
|
+
const report = validateTaskReport(deliveryRoot, task);
|
|
233
|
+
if (report.status !== 'complete') {
|
|
234
|
+
throw new Error(`Cannot complete ${lease.task_id}: task report is incomplete (${report.errors.join('; ')}).`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
database.prepare(`
|
|
238
|
+
UPDATE execution_leases SET status = 'released', released_at = ?, outcome = ? WHERE id = ?
|
|
239
|
+
`).run(at, outcome, leaseId);
|
|
240
|
+
const released = database.prepare('SELECT * FROM execution_leases WHERE id = ?').get(leaseId);
|
|
241
|
+
database.exec('COMMIT;');
|
|
242
|
+
return rowToLease(released);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
database.exec('ROLLBACK;');
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function listExecutionLeases(projectRoot, options = {}) {
|
|
251
|
+
const at = timestamp(options.now);
|
|
252
|
+
return withDatabase(projectRoot, (database) => {
|
|
253
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
254
|
+
try {
|
|
255
|
+
expireStale(database, at);
|
|
256
|
+
const rows = database.prepare(`
|
|
257
|
+
SELECT * FROM execution_leases
|
|
258
|
+
WHERE (? = 0 OR status = 'active')
|
|
259
|
+
AND (? = '' OR work_item_id = ?)
|
|
260
|
+
ORDER BY acquired_at DESC
|
|
261
|
+
`).all(options.activeOnly === true ? 1 : 0, options.workItemId ?? '', options.workItemId ?? '');
|
|
262
|
+
database.exec('COMMIT;');
|
|
263
|
+
return rows.map((row) => {
|
|
264
|
+
const lease = rowToLease(row);
|
|
265
|
+
if (options.includeTokens !== true) delete lease.token;
|
|
266
|
+
return lease;
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
database.exec('ROLLBACK;');
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Conductor recovery deliberately does not require the private capability token: it is
|
|
276
|
+
// scoped to one durable run ID and can only abandon leases, never complete work.
|
|
277
|
+
export function abandonExecutionLeasesForRun(projectRoot, runId, input = {}) {
|
|
278
|
+
const requiredRunId = required(runId, 'runId');
|
|
279
|
+
const at = timestamp(input.now);
|
|
280
|
+
const outcome = String(input.outcome ?? 'conductor-recovered').trim();
|
|
281
|
+
return withDatabase(projectRoot, (database) => {
|
|
282
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
283
|
+
try {
|
|
284
|
+
expireStale(database, at);
|
|
285
|
+
const rows = database.prepare(`
|
|
286
|
+
SELECT * FROM execution_leases WHERE run_id = ? AND status = 'active'
|
|
287
|
+
`).all(requiredRunId);
|
|
288
|
+
database.prepare(`
|
|
289
|
+
UPDATE execution_leases SET status = 'released', released_at = ?, outcome = ?
|
|
290
|
+
WHERE run_id = ? AND status = 'active'
|
|
291
|
+
`).run(at, outcome, requiredRunId);
|
|
292
|
+
database.exec('COMMIT;');
|
|
293
|
+
return rows.map((row) => ({ ...rowToLease(row), status: 'released', releasedAt: at, outcome }));
|
|
294
|
+
} catch (error) {
|
|
295
|
+
database.exec('ROLLBACK;');
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { atomicJson } from '../delivery-documents.mjs';
|
|
5
|
+
import {
|
|
6
|
+
commitProjectDiscovery,
|
|
7
|
+
discoveryQuestionnaire,
|
|
8
|
+
prepareProjectDiscovery
|
|
9
|
+
} from '../discovery.mjs';
|
|
10
|
+
import {
|
|
11
|
+
listOrganisationBlueprints,
|
|
12
|
+
projectOrganisationBlueprint,
|
|
13
|
+
resolveOrganisationBlueprint,
|
|
14
|
+
} from '../organisation-blueprints.mjs';
|
|
15
|
+
import {
|
|
16
|
+
ORGANISATION_POLICY_BASELINE_PATH,
|
|
17
|
+
materialisePolicyBaseline,
|
|
18
|
+
previewPolicyBaseline,
|
|
19
|
+
readPolicyBaselineStatus,
|
|
20
|
+
resolveOrganisationPolicy,
|
|
21
|
+
} from '../organisation-policies.mjs';
|
|
22
|
+
import { loadProjectConfig } from '../project.mjs';
|
|
23
|
+
import { selectContextualPersonas } from './persona-engagement.mjs';
|
|
24
|
+
import { readEvidenceDepthWorkspace } from './evidence-depth-workspace.mjs';
|
|
25
|
+
import { runtimePaths } from './paths.mjs';
|
|
26
|
+
|
|
27
|
+
const DRAFT_SCHEMA = 'ewai.guided-discovery-draft/v1';
|
|
28
|
+
const RESPONSE_SCHEMA = 'ewai.guided-discovery/v1';
|
|
29
|
+
|
|
30
|
+
function failure(message, statusCode = 409) {
|
|
31
|
+
const error = new Error(message);
|
|
32
|
+
error.statusCode = statusCode;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cleanText(value) {
|
|
37
|
+
return String(value ?? '').trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sectionContract(sectionId) {
|
|
41
|
+
const questionnaire = discoveryQuestionnaire();
|
|
42
|
+
const section = questionnaire.sections.find((candidate) => candidate.id === sectionId);
|
|
43
|
+
if (!section) throw failure(`Unknown discovery section: ${sectionId}`, 400);
|
|
44
|
+
return { questionnaire, section };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function selectDiscoveryPersonas(sectionId, personaCatalogue = [], answers = {}) {
|
|
48
|
+
const { section } = sectionContract(sectionId);
|
|
49
|
+
return selectContextualPersonas({
|
|
50
|
+
contextLabel: section.title,
|
|
51
|
+
signals: section.perspectiveSignals,
|
|
52
|
+
context: answers,
|
|
53
|
+
personaCatalogue,
|
|
54
|
+
limit: 4
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function initialAnswers(projectRoot) {
|
|
59
|
+
const { config } = loadProjectConfig(projectRoot);
|
|
60
|
+
return {
|
|
61
|
+
schema: 'ewai.discovery-answers/v1',
|
|
62
|
+
project: { name: config.project?.name ?? '' },
|
|
63
|
+
delivery: {},
|
|
64
|
+
assurance: {}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function initialDraft(projectRoot) {
|
|
69
|
+
return {
|
|
70
|
+
schema: DRAFT_SCHEMA,
|
|
71
|
+
revision: 0,
|
|
72
|
+
currentSection: 'purpose',
|
|
73
|
+
answers: initialAnswers(projectRoot),
|
|
74
|
+
preparedAt: null,
|
|
75
|
+
createdAt: null,
|
|
76
|
+
updatedAt: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readDraft(projectRoot) {
|
|
81
|
+
const { guidedDiscoveryDraftPath } = runtimePaths(projectRoot);
|
|
82
|
+
if (!existsSync(guidedDiscoveryDraftPath)) return initialDraft(projectRoot);
|
|
83
|
+
let draft;
|
|
84
|
+
try {
|
|
85
|
+
draft = JSON.parse(readFileSync(guidedDiscoveryDraftPath, 'utf8'));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw failure(`Guided discovery draft is not valid JSON: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
if (draft.schema !== DRAFT_SCHEMA || !Number.isInteger(draft.revision) || draft.revision < 1) {
|
|
90
|
+
throw failure('Guided discovery draft has an unsupported schema or revision');
|
|
91
|
+
}
|
|
92
|
+
sectionContract(draft.currentSection);
|
|
93
|
+
return draft;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function discoveryEvidenceDimension(question) {
|
|
97
|
+
const path = question.answerPath;
|
|
98
|
+
if (path === 'assurance.dataClassification' || /personalData|sensitiveData|jurisdictions/.test(path)) return 'data';
|
|
99
|
+
if (/authentication|multiTenant|internetFacing|payments/.test(path)) return 'security';
|
|
100
|
+
if (/availability/.test(path)) return 'operations';
|
|
101
|
+
if (path.startsWith('assurance.')) return 'governance';
|
|
102
|
+
if (/technology|deploymentTarget/.test(path)) return 'architecture';
|
|
103
|
+
if (path.startsWith('delivery.') || /definitionOfReady|definitionOfDone|ownership\.delivery/.test(path)) return 'delivery';
|
|
104
|
+
if (/hardConstraints|nonNegotiables|dontTouch|risks/.test(path)) return 'governance';
|
|
105
|
+
if (/ownership\.technical|inScope|capabilities|productTypes/.test(path)) return 'architecture';
|
|
106
|
+
return 'product';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasDiscoveryEvidence(value) {
|
|
110
|
+
if (Array.isArray(value)) return value.some((item) => cleanText(item));
|
|
111
|
+
if (value && typeof value === 'object') return Object.keys(value).length > 0;
|
|
112
|
+
return cleanText(value).length > 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function discoveryEvidenceDigest(value) {
|
|
116
|
+
const canonical = Array.isArray(value)
|
|
117
|
+
? value.map((item) => cleanText(item)).filter(Boolean).sort()
|
|
118
|
+
: value && typeof value === 'object'
|
|
119
|
+
? Object.fromEntries(Object.keys(value).sort().map((key) => [key, value[key]]))
|
|
120
|
+
: cleanText(value);
|
|
121
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(canonical)).digest('hex')}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function guidedDiscoveryOwnerEvidence(projectRoot) {
|
|
125
|
+
const draft = readDraft(projectRoot);
|
|
126
|
+
const entries = discoveryQuestionnaire().sections.flatMap((section) => section.questions)
|
|
127
|
+
.filter((question) => hasDiscoveryEvidence(question.answerPath.split('.').reduce((value, key) => value?.[key], draft.answers)))
|
|
128
|
+
.map((question) => {
|
|
129
|
+
const value = question.answerPath.split('.').reduce((current, key) => current?.[key], draft.answers);
|
|
130
|
+
return {
|
|
131
|
+
id: `discovery:${question.id}`,
|
|
132
|
+
dimension: discoveryEvidenceDimension(question),
|
|
133
|
+
authority: 'declared',
|
|
134
|
+
evidenceDigest: discoveryEvidenceDigest(value),
|
|
135
|
+
answerCode: `${question.id}-answer-recorded`,
|
|
136
|
+
reasonCode: `guided-discovery-r${draft.revision}`,
|
|
137
|
+
contradiction: 'none',
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
return entries.slice(0, 50);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validationPath(error) {
|
|
144
|
+
const required = error.message.match(/^Discovery answer is required: (.+)$/);
|
|
145
|
+
return required?.[1] ?? '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function organisationBlueprintCatalogue(projectRoot, options) {
|
|
149
|
+
return options.organisationBlueprints ?? listOrganisationBlueprints({
|
|
150
|
+
projectRoot,
|
|
151
|
+
roots: options.organisationBlueprintRoots,
|
|
152
|
+
home: options.home
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function prepareDraft(projectRoot, draft, options = {}, catalogue = organisationBlueprintCatalogue(projectRoot, options)) {
|
|
157
|
+
try {
|
|
158
|
+
const prepared = prepareProjectDiscovery(projectRoot, draft.answers, {
|
|
159
|
+
now: draft.preparedAt ?? undefined,
|
|
160
|
+
organisationBlueprints: catalogue
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
validation: { valid: true, errors: [] },
|
|
164
|
+
preview: {
|
|
165
|
+
schema: prepared.schema,
|
|
166
|
+
preparedAt: prepared.preparedAt,
|
|
167
|
+
outputs: prepared.outputs.map(({ kind, relative, exists }) => ({ kind, relative, exists })),
|
|
168
|
+
conflicts: prepared.conflicts,
|
|
169
|
+
selectedPacks: prepared.selectedPacks,
|
|
170
|
+
detectedPacks: prepared.detectedPacks,
|
|
171
|
+
organisationBlueprint: prepared.organisationBlueprint,
|
|
172
|
+
minimumStandards: prepared.minimumStandards,
|
|
173
|
+
complianceReviews: prepared.complianceReviews
|
|
174
|
+
},
|
|
175
|
+
prepared
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
validation: { valid: false, errors: [{ path: validationPath(error), message: error.message }] },
|
|
180
|
+
preview: null,
|
|
181
|
+
prepared: null
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function draftPolicyBaseline(draft, catalogue) {
|
|
187
|
+
const selection = draft.answers?.delivery?.organisationBlueprint;
|
|
188
|
+
if (!selection?.packId) return { resolved: null, preview: previewPolicyBaseline(null) };
|
|
189
|
+
try {
|
|
190
|
+
const blueprint = resolveOrganisationBlueprint(selection.packId, catalogue, {
|
|
191
|
+
enabledModules: selection.enabledModules ?? [],
|
|
192
|
+
});
|
|
193
|
+
const resolved = resolveOrganisationPolicy(blueprint);
|
|
194
|
+
return { resolved, preview: previewPolicyBaseline(resolved) };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return {
|
|
197
|
+
resolved: null,
|
|
198
|
+
preview: {
|
|
199
|
+
schema: 'ewai.policy-baseline-preview/v1',
|
|
200
|
+
status: 'invalid',
|
|
201
|
+
enabled: false,
|
|
202
|
+
blocking: false,
|
|
203
|
+
notice: error.message,
|
|
204
|
+
counts: { policies: 0, rules: 0 },
|
|
205
|
+
policies: [],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function guidedResponse(projectRoot, draft, options = {}) {
|
|
212
|
+
const { questionnaire, section } = sectionContract(draft.currentSection);
|
|
213
|
+
const catalogue = organisationBlueprintCatalogue(projectRoot, options);
|
|
214
|
+
const { validation, preview } = prepareDraft(projectRoot, draft, options, catalogue);
|
|
215
|
+
const policyBaseline = draftPolicyBaseline(draft, catalogue).preview;
|
|
216
|
+
const activePersonas = selectDiscoveryPersonas(section.id, options.personas ?? [], draft.answers);
|
|
217
|
+
const evidenceDepth = (() => {
|
|
218
|
+
try {
|
|
219
|
+
return readEvidenceDepthWorkspace(projectRoot, {
|
|
220
|
+
sourceMap: options.sourceMap,
|
|
221
|
+
personas: options.personas ?? [],
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return {
|
|
225
|
+
schema: 'ewai.evidence-depth-workspace/v1',
|
|
226
|
+
status: 'unavailable',
|
|
227
|
+
preparation: null,
|
|
228
|
+
questions: [],
|
|
229
|
+
activePersonas: [],
|
|
230
|
+
runs: [],
|
|
231
|
+
recovery: {
|
|
232
|
+
command: 'node bin/ewai archaeology depth-status --project .',
|
|
233
|
+
label: 'Inspect evidence depth',
|
|
234
|
+
},
|
|
235
|
+
reason: error.message,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
})();
|
|
239
|
+
return {
|
|
240
|
+
schema: RESPONSE_SCHEMA,
|
|
241
|
+
questionnaire,
|
|
242
|
+
draft,
|
|
243
|
+
validation,
|
|
244
|
+
preview,
|
|
245
|
+
organisationBlueprints: catalogue.map(projectOrganisationBlueprint),
|
|
246
|
+
policyBaseline,
|
|
247
|
+
activePersonas,
|
|
248
|
+
evidenceDepth,
|
|
249
|
+
guidance: {
|
|
250
|
+
advisory: true,
|
|
251
|
+
humanEvidenceTakesPriority: true,
|
|
252
|
+
section: section.id,
|
|
253
|
+
prompts: activePersonas.map((persona) => ({ personaId: persona.id, text: persona.engagementReason }))
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function readGuidedDiscovery(projectRoot, options = {}) {
|
|
259
|
+
return guidedResponse(projectRoot, readDraft(projectRoot), options);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function saveGuidedDiscoveryDraft(projectRoot, input = {}, options = {}) {
|
|
263
|
+
const current = readDraft(projectRoot);
|
|
264
|
+
const expectedRevision = Number(input.expectedRevision);
|
|
265
|
+
if (!Number.isInteger(expectedRevision) || expectedRevision !== current.revision) {
|
|
266
|
+
throw failure(`A newer revision of this guided discovery draft exists (current revision ${current.revision})`);
|
|
267
|
+
}
|
|
268
|
+
const { section } = sectionContract(cleanText(input.currentSection) || current.currentSection);
|
|
269
|
+
if (!input.answers || typeof input.answers !== 'object' || Array.isArray(input.answers)) {
|
|
270
|
+
throw failure('Guided discovery answers must be an object', 400);
|
|
271
|
+
}
|
|
272
|
+
const timestamp = options.now ?? new Date().toISOString();
|
|
273
|
+
const draft = {
|
|
274
|
+
schema: DRAFT_SCHEMA,
|
|
275
|
+
revision: current.revision + 1,
|
|
276
|
+
currentSection: section.id,
|
|
277
|
+
answers: structuredClone(input.answers),
|
|
278
|
+
preparedAt: timestamp,
|
|
279
|
+
createdAt: current.createdAt ?? timestamp,
|
|
280
|
+
updatedAt: timestamp
|
|
281
|
+
};
|
|
282
|
+
atomicJson(runtimePaths(projectRoot).guidedDiscoveryDraftPath, draft);
|
|
283
|
+
return guidedResponse(projectRoot, draft, options);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function approveGuidedDiscovery(projectRoot, input = {}, options = {}) {
|
|
287
|
+
if (input.confirmed !== true) throw failure('Canonical discovery requires explicit confirmation');
|
|
288
|
+
const approvedBy = cleanText(input.approvedBy);
|
|
289
|
+
if (!approvedBy) throw failure('Canonical discovery requires an accountable approver name');
|
|
290
|
+
const draft = readDraft(projectRoot);
|
|
291
|
+
const expectedRevision = Number(input.expectedRevision);
|
|
292
|
+
if (!Number.isInteger(expectedRevision) || expectedRevision !== draft.revision) {
|
|
293
|
+
throw failure(`A newer revision of this guided discovery draft exists (current revision ${draft.revision})`);
|
|
294
|
+
}
|
|
295
|
+
const { validation, prepared } = prepareDraft(projectRoot, draft, options);
|
|
296
|
+
if (!validation.valid || !prepared) throw failure('The current guided discovery draft is incomplete or invalid');
|
|
297
|
+
if (prepared.conflicts.length) {
|
|
298
|
+
throw failure(`Discovery outputs already exist and cannot be overwritten: ${prepared.conflicts.join(', ')}`);
|
|
299
|
+
}
|
|
300
|
+
const approvedAt = options.now ?? new Date().toISOString();
|
|
301
|
+
const catalogue = organisationBlueprintCatalogue(projectRoot, options);
|
|
302
|
+
const policy = draftPolicyBaseline(draft, catalogue);
|
|
303
|
+
if (policy.preview.status === 'invalid') throw failure(`The selected organisation policy is invalid: ${policy.preview.notice}`);
|
|
304
|
+
const commit = options.commit ?? commitProjectDiscovery;
|
|
305
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
306
|
+
const originalConfig = readFileSync(paths.configPath, 'utf8');
|
|
307
|
+
const baselinePath = resolve(paths.projectRoot, ORGANISATION_POLICY_BASELINE_PATH);
|
|
308
|
+
const hadBaseline = existsSync(baselinePath);
|
|
309
|
+
const originalBaseline = hadBaseline ? readFileSync(baselinePath, 'utf8') : null;
|
|
310
|
+
const materialise = options.materialisePolicy ?? materialisePolicyBaseline;
|
|
311
|
+
let policyBaseline;
|
|
312
|
+
let policyApplied = false;
|
|
313
|
+
let result;
|
|
314
|
+
try {
|
|
315
|
+
policyBaseline = policy.resolved?.contributions.length
|
|
316
|
+
? materialise(projectRoot, policy.resolved, {
|
|
317
|
+
confirmed: true,
|
|
318
|
+
approvedBy,
|
|
319
|
+
expectedDigest: policy.resolved.effectiveDigest,
|
|
320
|
+
now: approvedAt,
|
|
321
|
+
})
|
|
322
|
+
: readPolicyBaselineStatus(projectRoot);
|
|
323
|
+
policyApplied = Boolean(policy.resolved?.contributions.length);
|
|
324
|
+
result = commit(projectRoot, prepared, {
|
|
325
|
+
approvedBy,
|
|
326
|
+
approvedAt,
|
|
327
|
+
organisationBlueprints: options.organisationBlueprints,
|
|
328
|
+
organisationBlueprintRoots: options.organisationBlueprintRoots,
|
|
329
|
+
home: options.home,
|
|
330
|
+
personas: selectDiscoveryPersonas('review', options.personas ?? [], draft.answers),
|
|
331
|
+
});
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (policyApplied) {
|
|
334
|
+
writeFileSync(paths.configPath, originalConfig, 'utf8');
|
|
335
|
+
if (hadBaseline) writeFileSync(baselinePath, originalBaseline, 'utf8');
|
|
336
|
+
else rmSync(baselinePath, { force: true });
|
|
337
|
+
}
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
rmSync(runtimePaths(projectRoot).guidedDiscoveryDraftPath, { force: true });
|
|
341
|
+
return {
|
|
342
|
+
schema: 'ewai.guided-discovery-approval/v1',
|
|
343
|
+
status: 'completed',
|
|
344
|
+
approval: { approvedBy, approvedAt, revision: draft.revision },
|
|
345
|
+
created: result.created,
|
|
346
|
+
organisationBlueprint: result.organisationBlueprint,
|
|
347
|
+
policyBaseline,
|
|
348
|
+
nextCommand: result.nextCommand
|
|
349
|
+
};
|
|
350
|
+
}
|