@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,1314 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve } from 'node:path';
|
|
12
|
+
import YAML from 'yaml';
|
|
13
|
+
import { listPacks } from './packs.mjs';
|
|
14
|
+
import {
|
|
15
|
+
listOrganisationBlueprints,
|
|
16
|
+
projectOrganisationBlueprint,
|
|
17
|
+
resolveOrganisationBlueprint
|
|
18
|
+
} from './organisation-blueprints.mjs';
|
|
19
|
+
import { loadProjectConfig } from './project.mjs';
|
|
20
|
+
import {
|
|
21
|
+
VALIDATION_CHECKPOINTS,
|
|
22
|
+
VALIDATION_PROVIDERS,
|
|
23
|
+
normaliseValidationConfig,
|
|
24
|
+
} from './validation-config.mjs';
|
|
25
|
+
import { publishLifecycleEventSafely } from './runtime/lifecycle-hooks.mjs';
|
|
26
|
+
|
|
27
|
+
const ANSWERS_SCHEMA = 'ewai.discovery-answers/v1';
|
|
28
|
+
const TRI_STATE = new Set(['yes', 'no', 'unknown']);
|
|
29
|
+
const DATA_CLASSIFICATIONS = new Set(['public', 'internal', 'confidential', 'restricted', 'unknown']);
|
|
30
|
+
const AVAILABILITY_LEVELS = new Set(['standard', 'high', 'critical', 'unknown']);
|
|
31
|
+
const PROJECT_STAGES = new Set(['new', 'existing', 'modernisation', 'unknown']);
|
|
32
|
+
const VALIDATOR_STATES = new Set(['available', 'unavailable']);
|
|
33
|
+
|
|
34
|
+
const questionnaireSections = [
|
|
35
|
+
{
|
|
36
|
+
id: 'purpose',
|
|
37
|
+
title: 'Purpose',
|
|
38
|
+
description: 'Establish why the project exists and what has become unacceptable.',
|
|
39
|
+
perspectiveSignals: ['purpose', 'problem framing', 'strategy', 'business outcomes', 'evidence', 'decisions'],
|
|
40
|
+
questions: [
|
|
41
|
+
{ id: 'project-name', answerPath: 'project.name', label: 'Project name', prompt: 'What should people call this project?', help: 'Use the recognisable working name rather than a system code.', type: 'text', required: true },
|
|
42
|
+
{ id: 'project-purpose', answerPath: 'project.purpose', label: 'Purpose', prompt: 'What is this project for?', help: 'Describe the meaningful change the project should create.', type: 'textarea', required: true },
|
|
43
|
+
{ id: 'project-problem', answerPath: 'project.problem', label: 'Problem', prompt: 'What problem or unmet need does it address?', help: 'Describe the current reality without jumping to a solution.', type: 'textarea', required: true },
|
|
44
|
+
{ id: 'project-why-now', answerPath: 'project.whyNow', label: 'Why now', prompt: 'What changed or became unacceptable?', help: 'Name the pressure, opportunity, or evidence that makes action timely.', type: 'textarea', required: false }
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'people',
|
|
49
|
+
title: 'People',
|
|
50
|
+
description: 'Name the people affected and the humans accountable for the work.',
|
|
51
|
+
perspectiveSignals: ['end user', 'service design', 'accessibility', 'operations', 'stakeholder'],
|
|
52
|
+
questions: [
|
|
53
|
+
{ id: 'primary-users', answerPath: 'project.primaryUsers', label: 'People and their situation', prompt: 'Who are the main people this project must help?', help: 'Add one recognisable role or group per line, including the situation they face.', type: 'list', required: true },
|
|
54
|
+
{ id: 'product-owner', answerPath: 'project.ownership.product', label: 'Product owner', prompt: 'Who owns whether the outcomes succeed or fail?', help: 'Use a name or accountable role.', type: 'text', required: false },
|
|
55
|
+
{ id: 'delivery-owner', answerPath: 'project.ownership.delivery', label: 'Delivery owner', prompt: 'Who owns delivery and quality?', help: 'Use a name or accountable role.', type: 'text', required: false },
|
|
56
|
+
{ id: 'technical-owner', answerPath: 'project.ownership.technical', label: 'Technical owner', prompt: 'Who owns architecture and technical constraints?', help: 'Use a name or accountable role.', type: 'text', required: false }
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'outcomes',
|
|
61
|
+
title: 'Outcomes',
|
|
62
|
+
description: 'Define observable success, failure, readiness, and completion.',
|
|
63
|
+
perspectiveSignals: ['outcomes', 'product owner', 'measurement', 'quality', 'testing'],
|
|
64
|
+
questions: [
|
|
65
|
+
{ id: 'desired-outcomes', answerPath: 'project.desiredOutcomes', label: 'Desired outcomes', prompt: 'What should become meaningfully better?', help: 'Add one observable outcome per line.', type: 'list', required: true },
|
|
66
|
+
{ id: 'success-signals', answerPath: 'project.successSignals', label: 'Success signals', prompt: 'What would show the project is working?', help: 'Use evidence someone could observe or measure.', type: 'list', required: false },
|
|
67
|
+
{ id: 'failure-signals', answerPath: 'project.failureSignals', label: 'Failure signals', prompt: 'How could this project quietly fail?', help: 'Name early warning signs, not only catastrophic outcomes.', type: 'list', required: false },
|
|
68
|
+
{ id: 'definition-ready', answerPath: 'project.definitionOfReady', label: 'Ready for Build', prompt: 'What must be true before implementation begins?', help: 'Include decisions, owners, evidence, or dependencies.', type: 'list', required: false },
|
|
69
|
+
{ id: 'definition-done', answerPath: 'project.definitionOfDone', label: 'Definition of Done', prompt: 'What evidence is needed to call this complete?', help: 'Include testing, acceptance, operational, and learning evidence.', type: 'list', required: false }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'scope',
|
|
74
|
+
title: 'Scope',
|
|
75
|
+
description: 'Make boundaries, assumptions, risks, and protected areas explicit.',
|
|
76
|
+
perspectiveSignals: ['scope', 'business analyst', 'architecture', 'risk', 'delivery'],
|
|
77
|
+
questions: [
|
|
78
|
+
{ id: 'in-scope', answerPath: 'project.inScope', label: 'In scope', prompt: 'What is explicitly part of this project?', help: 'Use one clear boundary or capability per line.', type: 'list', required: false },
|
|
79
|
+
{ id: 'capabilities', answerPath: 'project.capabilities', label: 'Expected capabilities', prompt: 'What must the project be able to do?', help: 'Record current beliefs; Discovery can challenge them later.', type: 'list', required: false },
|
|
80
|
+
{ id: 'missing-capabilities', answerPath: 'project.missingCapabilities', label: 'Possible gaps', prompt: 'What might disappointed users say it cannot do?', help: 'Capture blind spots or missing capabilities.', type: 'list', required: false },
|
|
81
|
+
{ id: 'project-stage', answerPath: 'project.stage', label: 'Project stage', prompt: 'What kind of starting point is this?', help: 'Choose the closest current state.', type: 'select', required: false, options: ['new', 'existing', 'modernisation', 'unknown'] },
|
|
82
|
+
{ id: 'product-types', answerPath: 'project.productTypes', label: 'Product types', prompt: 'What kinds of product or service are involved?', help: 'For example: web application, API, CLI, workflow, or policy.', type: 'list', required: false },
|
|
83
|
+
{ id: 'non-goals', answerPath: 'project.nonGoals', label: 'Non-goals', prompt: 'What will this project deliberately not do?', help: 'Clear exclusions protect focus and future decisions.', type: 'list', required: false },
|
|
84
|
+
{ id: 'dont-touch', answerPath: 'project.dontTouch', label: 'Do not touch', prompt: 'What systems, code, data, or processes must remain untouched?', help: 'Name protected boundaries explicitly.', type: 'list', required: false },
|
|
85
|
+
{ id: 'hard-constraints', answerPath: 'project.hardConstraints', label: 'Hard constraints', prompt: 'Which constraints are confirmed facts?', help: 'Include fixed dates, contracts, platforms, or other binding realities.', type: 'list', required: false },
|
|
86
|
+
{ id: 'soft-constraints', answerPath: 'project.softConstraints', label: 'Assumptions to test', prompt: 'Which inherited constraints may only be assumptions?', help: 'Record them without promoting them to facts.', type: 'list', required: false },
|
|
87
|
+
{ id: 'non-negotiables', answerPath: 'project.nonNegotiables', label: 'Non-negotiables', prompt: 'Which project rules cannot be traded away?', help: 'Use one rule per line.', type: 'list', required: false },
|
|
88
|
+
{ id: 'open-questions', answerPath: 'project.openQuestions', label: 'Open questions', prompt: 'What still blocks confident planning?', help: 'Name the question and, where known, who can answer it.', type: 'list', required: false },
|
|
89
|
+
{ id: 'risks', answerPath: 'project.risks', label: 'Risks and assumptions', prompt: 'What could invalidate the current direction?', help: 'Include delivery, user, technical, data, or organisational risks.', type: 'list', required: false },
|
|
90
|
+
{ id: 'one-sentence', answerPath: 'project.oneSentence', label: 'In one sentence', prompt: 'How would you describe this project honestly in one sentence?', help: 'Use plain language that a new collaborator would recognise.', type: 'text', required: false },
|
|
91
|
+
{ id: 'avoid-becoming', answerPath: 'project.avoidBecoming', label: 'Avoid becoming', prompt: 'What is the one thing this project must not become?', help: 'Use this as a guardrail against drift.', type: 'textarea', required: false }
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'delivery',
|
|
96
|
+
title: 'Delivery',
|
|
97
|
+
description: 'Record technology direction without turning candidates into binding decisions.',
|
|
98
|
+
perspectiveSignals: ['software maintainer', 'architecture', 'delivery', 'operations', 'technology'],
|
|
99
|
+
questions: [
|
|
100
|
+
{ id: 'technology-packs', answerPath: 'delivery.technologyPacks', label: 'Technology packs', prompt: 'Which installed EWAI technology packs should guide the project?', help: 'Leave empty when technology remains undecided.', type: 'list', required: false },
|
|
101
|
+
{ id: 'custom-technology', answerPath: 'delivery.customTechnology', label: 'Custom technology', prompt: 'Which technologies are not represented by an installed pack?', help: 'Record candidates without implying approval.', type: 'list', required: false },
|
|
102
|
+
{ id: 'deployment-target', answerPath: 'delivery.deploymentTarget', label: 'Deployment direction', prompt: 'Where is the project expected to run?', help: 'Use “undecided” when the answer is not yet owned.', type: 'text', required: false },
|
|
103
|
+
{ id: 'delivery-constraints', answerPath: 'delivery.constraints', label: 'Delivery constraints', prompt: 'Which technical or delivery constraints shape the work?', help: 'Separate confirmed constraints from preferences.', type: 'list', required: false },
|
|
104
|
+
{ id: 'organisation-blueprint', answerPath: 'delivery.organisationBlueprint', label: 'Organisation blueprint', prompt: 'Should this project adopt an installed organisation blueprint?', help: 'Choose one reviewed local blueprint and any optional modules. Nothing is applied until named approval.', type: 'organisation-blueprint', required: false }
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'assurance',
|
|
109
|
+
title: 'Assurance',
|
|
110
|
+
description: 'Triage data, security, accessibility, resilience, and regulatory concerns.',
|
|
111
|
+
perspectiveSignals: ['security', 'privacy', 'compliance', 'accessibility', 'resilience', 'operations', 'recovery', 'assurance'],
|
|
112
|
+
questions: [
|
|
113
|
+
{ id: 'jurisdictions', answerPath: 'assurance.jurisdictions', label: 'Jurisdictions', prompt: 'Where will the organisation, users, infrastructure, or affected data be located?', help: 'Unknown is acceptable; uncertainty should remain visible.', type: 'list', required: false },
|
|
114
|
+
{ id: 'regulated-domains', answerPath: 'assurance.regulatedDomains', label: 'Controlled domains', prompt: 'Which regulated or contract-controlled domains may apply?', help: 'Record candidates for qualified review.', type: 'list', required: false },
|
|
115
|
+
{ id: 'data-classification', answerPath: 'assurance.dataClassification', label: 'Data classification', prompt: 'What is the highest expected data classification?', help: 'Choose the most cautious currently supported answer.', type: 'select', required: false, options: ['public', 'internal', 'confidential', 'restricted', 'unknown'] },
|
|
116
|
+
...['personalData', 'sensitiveData', 'authentication', 'multiTenant', 'internetFacing', 'payments', 'aiFeatures', 'accessibility'].map((field) => ({
|
|
117
|
+
id: field.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
|
|
118
|
+
answerPath: `assurance.${field}`,
|
|
119
|
+
label: ({ personalData: 'Personal data', sensitiveData: 'Sensitive data', authentication: 'Authentication', multiTenant: 'Multiple organisations', internetFacing: 'Internet-facing', payments: 'Payments', aiFeatures: 'AI features', accessibility: 'Accessibility obligations' })[field],
|
|
120
|
+
prompt: ({ personalData: 'Will it process personal data?', sensitiveData: 'Will it process sensitive or specially protected data?', authentication: 'Will users authenticate?', multiTenant: 'Will it serve more than one customer or organisation?', internetFacing: 'Will any service be exposed to the public internet?', payments: 'Will it accept or process payments?', aiFeatures: 'Will it include AI-assisted or automated behaviour?', accessibility: 'Are accessibility obligations expected?' })[field],
|
|
121
|
+
help: 'Choose unknown when the evidence is not yet available.',
|
|
122
|
+
type: 'select',
|
|
123
|
+
required: false,
|
|
124
|
+
options: ['yes', 'no', 'unknown']
|
|
125
|
+
})),
|
|
126
|
+
{ id: 'availability', answerPath: 'assurance.availability', label: 'Availability', prompt: 'How critical is service availability?', help: 'Choose unknown when service expectations are not yet owned.', type: 'select', required: false, options: ['standard', 'high', 'critical', 'unknown'] }
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'review',
|
|
131
|
+
title: 'Review & approve',
|
|
132
|
+
description: 'Review answers, candidate warnings, destinations, and accountable approval.',
|
|
133
|
+
perspectiveSignals: ['product owner', 'engineering manager', 'security', 'operations', 'quality'],
|
|
134
|
+
questions: []
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
export function discoveryQuestionnaire() {
|
|
139
|
+
return structuredClone({
|
|
140
|
+
schema: 'ewai.discovery-questionnaire/v1',
|
|
141
|
+
answersSchema: ANSWERS_SCHEMA,
|
|
142
|
+
sections: questionnaireSections,
|
|
143
|
+
inheritedPaths: ['delivery.detectedPacks', 'validation']
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const outputFiles = [
|
|
148
|
+
'1.Scope/context.md',
|
|
149
|
+
'1.Scope/project-scope.md',
|
|
150
|
+
'2.Purpose/intent-brief.md',
|
|
151
|
+
'2.Purpose/explorations/project-discovery.md',
|
|
152
|
+
'3.Evidence/success-criteria.md',
|
|
153
|
+
'3.Evidence/risk/compliance-applicability.md',
|
|
154
|
+
'4.Constraints/project-constraints.md',
|
|
155
|
+
'5.Strategy/approach.md',
|
|
156
|
+
'5.Strategy/architecture/stack.md',
|
|
157
|
+
'5.Strategy/options/minimum-standards.md'
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
function asString(value, fallback = '') {
|
|
161
|
+
if (value === undefined || value === null) return fallback;
|
|
162
|
+
return String(value).trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function asList(value) {
|
|
166
|
+
if (Array.isArray(value)) return value.map((item) => asString(item)).filter(Boolean);
|
|
167
|
+
const text = asString(value);
|
|
168
|
+
return text ? text.split(',').map((item) => item.trim()).filter(Boolean) : [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function asEnum(value, allowed, fallback) {
|
|
172
|
+
const normalised = asString(value, fallback).toLowerCase();
|
|
173
|
+
if (!allowed.has(normalised)) {
|
|
174
|
+
throw new Error(`Unsupported value "${value}"; expected one of: ${[...allowed].join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
return normalised;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function unique(values) {
|
|
180
|
+
return [...new Set(values)];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function fileMetadata(path) {
|
|
184
|
+
try {
|
|
185
|
+
return lstatSync(path);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error.code === 'ENOENT') return null;
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function safeSpecsDestination(specsRoot, relativeDestination) {
|
|
193
|
+
const destination = resolve(specsRoot, relativeDestination);
|
|
194
|
+
const bounded = relative(specsRoot, destination);
|
|
195
|
+
if (!bounded || bounded.startsWith('..') || isAbsolute(bounded)) {
|
|
196
|
+
throw new Error(`Discovery destination escapes the SPECS root: ${relativeDestination}`);
|
|
197
|
+
}
|
|
198
|
+
let cursor = specsRoot;
|
|
199
|
+
for (const segment of bounded.split(/[\\/]+/)) {
|
|
200
|
+
cursor = resolve(cursor, segment);
|
|
201
|
+
const metadata = fileMetadata(cursor);
|
|
202
|
+
if (metadata?.isSymbolicLink()) {
|
|
203
|
+
throw new Error(`Discovery destination may not traverse a symbolic link: ${relativeDestination}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return destination;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normaliseOrganisationBlueprint(value) {
|
|
210
|
+
if (value === undefined || value === null || value === '') return null;
|
|
211
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
212
|
+
throw new Error('delivery.organisationBlueprint must be a blueprint selection');
|
|
213
|
+
}
|
|
214
|
+
const packId = asString(value.packId);
|
|
215
|
+
if (!packId) return null;
|
|
216
|
+
return { packId, enabledModules: unique(asList(value.enabledModules)).sort() };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function discoveryValidation(raw, configured) {
|
|
220
|
+
const rawProviders = raw.providers ?? Object.fromEntries(
|
|
221
|
+
VALIDATION_PROVIDERS
|
|
222
|
+
.filter((provider) => raw[provider] !== undefined)
|
|
223
|
+
.map((provider) => [provider, raw[provider]]),
|
|
224
|
+
);
|
|
225
|
+
const rawCheckpoints = raw.checkpoints ?? {};
|
|
226
|
+
const checkpoints = Object.fromEntries(
|
|
227
|
+
Object.keys(VALIDATION_CHECKPOINTS).map((checkpoint) => {
|
|
228
|
+
const value = rawCheckpoints[checkpoint] ?? {};
|
|
229
|
+
return [
|
|
230
|
+
checkpoint,
|
|
231
|
+
{
|
|
232
|
+
...(configured.external?.checkpoints?.[checkpoint] ?? {}),
|
|
233
|
+
enabled: value.enabled
|
|
234
|
+
?? configured.external?.checkpoints?.[checkpoint]?.enabled,
|
|
235
|
+
max_cycles: value.max_cycles
|
|
236
|
+
?? configured.external?.checkpoints?.[checkpoint]?.max_cycles,
|
|
237
|
+
validators: value.validators
|
|
238
|
+
?? configured.external?.checkpoints?.[checkpoint]?.validators,
|
|
239
|
+
review: {
|
|
240
|
+
...(configured.external?.checkpoints?.[checkpoint]?.review ?? {}),
|
|
241
|
+
...(value.review ?? {}),
|
|
242
|
+
...(value.breadth ? { breadth: value.breadth } : {}),
|
|
243
|
+
...(value.depth ? { depth: value.depth } : {}),
|
|
244
|
+
...(value.output ? { output: value.output } : {}),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return normaliseValidationConfig({
|
|
252
|
+
...configured,
|
|
253
|
+
external: {
|
|
254
|
+
...(configured.external ?? {}),
|
|
255
|
+
providers: {
|
|
256
|
+
...(configured.external?.providers ?? {}),
|
|
257
|
+
...rawProviders,
|
|
258
|
+
},
|
|
259
|
+
checkpoints,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function requireAnswer(value, label) {
|
|
265
|
+
if (Array.isArray(value) ? value.length === 0 : !value) {
|
|
266
|
+
throw new Error(`Discovery answer is required: ${label}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function escapeTable(value) {
|
|
271
|
+
return asString(value, 'Not specified').replaceAll('|', '\\|').replaceAll('\n', '<br>');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function bullets(values, empty = 'Not specified') {
|
|
275
|
+
return values.length ? values.map((value) => `- ${value}`).join('\n') : `- ${empty}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function yes(value) {
|
|
279
|
+
return value === 'yes';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function yesOrUnknown(value) {
|
|
283
|
+
return value === 'yes' || value === 'unknown';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function walkDirectories(root, maximumDepth = 2, depth = 0) {
|
|
287
|
+
if (!existsSync(root) || depth > maximumDepth) return [];
|
|
288
|
+
const found = [root];
|
|
289
|
+
if (depth === maximumDepth) return found;
|
|
290
|
+
|
|
291
|
+
const ignored = new Set(['.git', '.ewai-pipeline', '.agents', '.claude', 'node_modules', 'vendor', 'SPECS']);
|
|
292
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
293
|
+
if (!entry.isDirectory() || ignored.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
294
|
+
found.push(...walkDirectories(resolve(root, entry.name), maximumDepth, depth + 1));
|
|
295
|
+
}
|
|
296
|
+
return found;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function safeDetectionPath(value, { pattern = false } = {}) {
|
|
300
|
+
const candidate = String(value ?? '').trim();
|
|
301
|
+
const segments = candidate.split('/');
|
|
302
|
+
if (!candidate || candidate.length > 240 || candidate.startsWith('/') || candidate.startsWith('!')
|
|
303
|
+
|| candidate.includes('\\') || candidate.includes('\0') || segments.includes('..') || /[{}()[\]]/.test(candidate)
|
|
304
|
+
|| (!pattern && /[*?]/.test(candidate))) {
|
|
305
|
+
throw new Error(`unsafe pack detection ${pattern ? 'pattern' : 'path'}: ${candidate || '<empty>'}`);
|
|
306
|
+
}
|
|
307
|
+
return candidate;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function detectionPatternRegex(value) {
|
|
311
|
+
const pattern = safeDetectionPath(value, { pattern: true });
|
|
312
|
+
let expression = '^';
|
|
313
|
+
for (let index = 0; index < pattern.length;) {
|
|
314
|
+
if (pattern[index] === '*' && pattern[index + 1] === '*') {
|
|
315
|
+
expression += pattern[index + 2] === '/' ? '(?:.*/)?' : '.*';
|
|
316
|
+
index += pattern[index + 2] === '/' ? 3 : 2;
|
|
317
|
+
} else if (pattern[index] === '*') {
|
|
318
|
+
expression += '[^/]*';
|
|
319
|
+
index += 1;
|
|
320
|
+
} else if (pattern[index] === '?') {
|
|
321
|
+
expression += '[^/]';
|
|
322
|
+
index += 1;
|
|
323
|
+
} else {
|
|
324
|
+
expression += pattern[index].replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
325
|
+
index += 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return new RegExp(`${expression}$`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function walkDetectionFiles(root, maximumDepth = 6, maximumFiles = 10_000) {
|
|
332
|
+
const ignored = new Set(['.git', '.ewai-pipeline', '.agents', '.claude', 'node_modules', 'vendor', 'SPECS']);
|
|
333
|
+
const found = [];
|
|
334
|
+
const visit = (directory, depth) => {
|
|
335
|
+
if (depth > maximumDepth || found.length >= maximumFiles) return;
|
|
336
|
+
let entries;
|
|
337
|
+
try {
|
|
338
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
339
|
+
} catch {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
if (found.length >= maximumFiles) break;
|
|
344
|
+
if (entry.isSymbolicLink()) continue;
|
|
345
|
+
const path = resolve(directory, entry.name);
|
|
346
|
+
if (entry.isDirectory()) {
|
|
347
|
+
if (!ignored.has(entry.name) && !entry.name.startsWith('.')) visit(path, depth + 1);
|
|
348
|
+
} else if (entry.isFile()) found.push(path);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
visit(root, 0);
|
|
352
|
+
return found;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function dependencyNames(directory) {
|
|
356
|
+
const names = new Set();
|
|
357
|
+
const packagePath = resolve(directory, 'package.json');
|
|
358
|
+
const composerPath = resolve(directory, 'composer.json');
|
|
359
|
+
|
|
360
|
+
if (existsSync(packagePath)) {
|
|
361
|
+
try {
|
|
362
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
363
|
+
for (const group of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
364
|
+
for (const name of Object.keys(packageJson[group] ?? {})) names.add(name);
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
// A malformed project manifest is reported by project tooling; detection remains best-effort.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (existsSync(composerPath)) {
|
|
372
|
+
try {
|
|
373
|
+
const composer = JSON.parse(readFileSync(composerPath, 'utf8'));
|
|
374
|
+
for (const group of ['require', 'require-dev']) {
|
|
375
|
+
for (const name of Object.keys(composer[group] ?? {})) names.add(name);
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
// See package.json handling above.
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return names;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function detectTechnologyPacks(projectRoot, packs = listPacks()) {
|
|
386
|
+
const root = resolve(projectRoot);
|
|
387
|
+
const directories = walkDirectories(root);
|
|
388
|
+
const regularFiles = walkDetectionFiles(root);
|
|
389
|
+
const detected = new Set();
|
|
390
|
+
const technologyPacks = packs.filter((pack) => pack.type === 'technology');
|
|
391
|
+
|
|
392
|
+
for (const pack of technologyPacks) {
|
|
393
|
+
const requiredFiles = (pack.detection?.files ?? []).map((value) => safeDetectionPath(value));
|
|
394
|
+
const requiredDependencies = pack.detection?.dependencies ?? [];
|
|
395
|
+
const anyFiles = (pack.detection?.any_files ?? []).map((value) => safeDetectionPath(value));
|
|
396
|
+
const anyPatterns = (pack.detection?.any_patterns ?? []).map(detectionPatternRegex);
|
|
397
|
+
const matched = directories.some((directory) => {
|
|
398
|
+
const filesMatch = requiredFiles.length > 0
|
|
399
|
+
&& requiredFiles.every((file) => existsSync(resolve(directory, file)));
|
|
400
|
+
const dependencies = dependencyNames(directory);
|
|
401
|
+
const dependenciesMatch = requiredDependencies.length > 0
|
|
402
|
+
&& requiredDependencies.every((dependency) => dependencies.has(dependency));
|
|
403
|
+
const anyFileMatches = anyFiles.some((file) => existsSync(resolve(directory, file)));
|
|
404
|
+
const anyPatternMatches = anyPatterns.length > 0 && regularFiles.some((file) => {
|
|
405
|
+
const candidate = relative(directory, file).replaceAll('\\', '/');
|
|
406
|
+
return !candidate.startsWith('../') && anyPatterns.some((pattern) => pattern.test(candidate));
|
|
407
|
+
});
|
|
408
|
+
return filesMatch || dependenciesMatch || anyFileMatches || anyPatternMatches;
|
|
409
|
+
});
|
|
410
|
+
if (matched) detected.add(pack.id);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const pack of packs.filter((candidate) => candidate.type === 'stack')) {
|
|
414
|
+
const requirements = pack.requires ?? [];
|
|
415
|
+
if (requirements.length && requirements.every((id) => detected.has(id))) detected.add(pack.id);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return [...detected];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function resolveSelectedPacks(selected, packs) {
|
|
422
|
+
const byId = new Map(packs.map((pack) => [pack.id, pack]));
|
|
423
|
+
const resolved = [];
|
|
424
|
+
const visit = (id) => {
|
|
425
|
+
const pack = byId.get(id);
|
|
426
|
+
if (!pack) throw new Error(`Unknown technology pack: ${id}`);
|
|
427
|
+
if (!['technology', 'stack'].includes(pack.type)) {
|
|
428
|
+
throw new Error(`Pack ${id} is not a technology or stack pack`);
|
|
429
|
+
}
|
|
430
|
+
for (const requirement of pack.requires ?? []) visit(requirement);
|
|
431
|
+
if (!resolved.includes(id)) resolved.push(id);
|
|
432
|
+
};
|
|
433
|
+
for (const id of selected) visit(id);
|
|
434
|
+
return resolved;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function normaliseDiscoveryAnswers(rawAnswers, projectConfig, detectedPacks = []) {
|
|
438
|
+
const raw = rawAnswers ?? {};
|
|
439
|
+
if (raw.schema && raw.schema !== ANSWERS_SCHEMA) {
|
|
440
|
+
throw new Error(`Unsupported discovery answers schema: ${raw.schema}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const project = raw.project ?? {};
|
|
444
|
+
const delivery = raw.delivery ?? {};
|
|
445
|
+
const assurance = raw.assurance ?? {};
|
|
446
|
+
const validation = raw.validation ?? {};
|
|
447
|
+
const configuredValidation = normaliseValidationConfig(projectConfig.validation);
|
|
448
|
+
const selectedPacks = unique(asList(delivery.technologyPacks ?? delivery.stack));
|
|
449
|
+
const answers = {
|
|
450
|
+
schema: ANSWERS_SCHEMA,
|
|
451
|
+
project: {
|
|
452
|
+
name: asString(project.name, projectConfig.project?.name),
|
|
453
|
+
purpose: asString(project.purpose),
|
|
454
|
+
whyNow: asString(project.whyNow, project.problem),
|
|
455
|
+
problem: asString(project.problem),
|
|
456
|
+
primaryUsers: asList(project.primaryUsers),
|
|
457
|
+
desiredOutcomes: asList(project.desiredOutcomes),
|
|
458
|
+
inScope: asList(project.inScope),
|
|
459
|
+
capabilities: asList(project.capabilities),
|
|
460
|
+
missingCapabilities: asList(project.missingCapabilities),
|
|
461
|
+
stage: asEnum(project.stage, PROJECT_STAGES, 'unknown'),
|
|
462
|
+
productTypes: asList(project.productTypes),
|
|
463
|
+
nonGoals: asList(project.nonGoals),
|
|
464
|
+
dontTouch: asList(project.dontTouch),
|
|
465
|
+
hardConstraints: asList(project.hardConstraints),
|
|
466
|
+
softConstraints: asList(project.softConstraints),
|
|
467
|
+
nonNegotiables: asList(project.nonNegotiables),
|
|
468
|
+
successSignals: asList(project.successSignals).length
|
|
469
|
+
? asList(project.successSignals)
|
|
470
|
+
: asList(project.desiredOutcomes),
|
|
471
|
+
failureSignals: asList(project.failureSignals),
|
|
472
|
+
definitionOfReady: asList(project.definitionOfReady),
|
|
473
|
+
definitionOfDone: asList(project.definitionOfDone),
|
|
474
|
+
openQuestions: asList(project.openQuestions),
|
|
475
|
+
risks: asList(project.risks),
|
|
476
|
+
oneSentence: asString(project.oneSentence, project.purpose),
|
|
477
|
+
avoidBecoming: asString(project.avoidBecoming),
|
|
478
|
+
ownership: {
|
|
479
|
+
product: asString(project.ownership?.product, 'Unassigned'),
|
|
480
|
+
delivery: asString(project.ownership?.delivery, 'Unassigned'),
|
|
481
|
+
technical: asString(project.ownership?.technical, 'Unassigned')
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
delivery: {
|
|
485
|
+
technologyPacks: selectedPacks,
|
|
486
|
+
customTechnology: asList(delivery.customTechnology),
|
|
487
|
+
deploymentTarget: asString(delivery.deploymentTarget, 'undecided'),
|
|
488
|
+
constraints: asList(delivery.constraints),
|
|
489
|
+
organisationBlueprint: normaliseOrganisationBlueprint(delivery.organisationBlueprint),
|
|
490
|
+
detectedPacks: unique(asList(delivery.detectedPacks).length ? asList(delivery.detectedPacks) : detectedPacks)
|
|
491
|
+
},
|
|
492
|
+
assurance: {
|
|
493
|
+
jurisdictions: asList(assurance.jurisdictions),
|
|
494
|
+
regulatedDomains: asList(assurance.regulatedDomains),
|
|
495
|
+
dataClassification: asEnum(assurance.dataClassification, DATA_CLASSIFICATIONS, 'unknown'),
|
|
496
|
+
personalData: asEnum(assurance.personalData, TRI_STATE, 'unknown'),
|
|
497
|
+
sensitiveData: asEnum(assurance.sensitiveData, TRI_STATE, 'unknown'),
|
|
498
|
+
authentication: asEnum(assurance.authentication, TRI_STATE, 'unknown'),
|
|
499
|
+
multiTenant: asEnum(assurance.multiTenant, TRI_STATE, 'unknown'),
|
|
500
|
+
internetFacing: asEnum(assurance.internetFacing, TRI_STATE, 'unknown'),
|
|
501
|
+
payments: asEnum(assurance.payments, TRI_STATE, 'unknown'),
|
|
502
|
+
aiFeatures: asEnum(assurance.aiFeatures, TRI_STATE, 'unknown'),
|
|
503
|
+
accessibility: asEnum(assurance.accessibility, TRI_STATE, 'unknown'),
|
|
504
|
+
availability: asEnum(assurance.availability, AVAILABILITY_LEVELS, 'unknown')
|
|
505
|
+
},
|
|
506
|
+
validation: discoveryValidation(validation, configuredValidation)
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
requireAnswer(answers.project.name, 'project.name');
|
|
510
|
+
requireAnswer(answers.project.purpose, 'project.purpose');
|
|
511
|
+
requireAnswer(answers.project.problem, 'project.problem');
|
|
512
|
+
requireAnswer(answers.project.primaryUsers, 'project.primaryUsers');
|
|
513
|
+
requireAnswer(answers.project.desiredOutcomes, 'project.desiredOutcomes');
|
|
514
|
+
|
|
515
|
+
return answers;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function parseMarkdownDocument(content) {
|
|
519
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
520
|
+
if (!match) return { metadata: {}, body: content.trim() };
|
|
521
|
+
let metadata = {};
|
|
522
|
+
try {
|
|
523
|
+
metadata = YAML.parse(match[1]) ?? {};
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new Error(`Organisation blueprint persona frontmatter is invalid: ${error.message}`);
|
|
526
|
+
}
|
|
527
|
+
return { metadata, body: content.slice(match[0].length).trim() };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function organisationPackSlug(pack) {
|
|
531
|
+
return pack.id.split('.').slice(2).join('-');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function renderOrganisationStandard(pack, module, standard, approval) {
|
|
535
|
+
const metadata = {
|
|
536
|
+
schema: 'ewai.organisation-standard/v1',
|
|
537
|
+
id: `${pack.id}.${module.id}.${standard.id}`,
|
|
538
|
+
title: standard.title,
|
|
539
|
+
source_pack: pack.id,
|
|
540
|
+
source_version: pack.version,
|
|
541
|
+
source_digest: pack.digest,
|
|
542
|
+
module: module.id,
|
|
543
|
+
approved_by: approval.approvedBy || 'Pending named approval',
|
|
544
|
+
approved_at: approval.approvedAt || 'Pending named approval'
|
|
545
|
+
};
|
|
546
|
+
return `---\n${YAML.stringify(metadata, { lineWidth: 0 }).trim()}\n---\n\n${standard.content.trim()}\n`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function renderOrganisationPersona(pack, item, approval) {
|
|
550
|
+
const { metadata: source, body } = parseMarkdownDocument(item.content);
|
|
551
|
+
const identity = `project.${pack.blueprint.publisher.id}.${item.id}`;
|
|
552
|
+
const metadata = {
|
|
553
|
+
schema: 'ewai.persona/v1',
|
|
554
|
+
id: identity,
|
|
555
|
+
name: item.name ?? source.name ?? item.id,
|
|
556
|
+
version: source.version ?? pack.version,
|
|
557
|
+
description: source.description ?? `Applies ${pack.name} organisation context to this project.`,
|
|
558
|
+
category: source.category ?? 'organisation',
|
|
559
|
+
pack: pack.id,
|
|
560
|
+
tier: 'project',
|
|
561
|
+
tags: Array.isArray(source.tags) ? source.tags : [],
|
|
562
|
+
capabilities: Array.isArray(source.capabilities) ? source.capabilities : [],
|
|
563
|
+
provenance: {
|
|
564
|
+
source_pack: pack.id,
|
|
565
|
+
source_version: pack.version,
|
|
566
|
+
source_digest: pack.digest,
|
|
567
|
+
approved_by: approval.approvedBy || 'Pending named approval',
|
|
568
|
+
approved_at: approval.approvedAt || 'Pending named approval'
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const title = `# ${metadata.name}`;
|
|
572
|
+
const renderedBody = body || `${title}\n\nApply the reviewed ${pack.name} perspective to project decisions.`;
|
|
573
|
+
return `---\n${YAML.stringify(metadata, { lineWidth: 0 }).trim()}\n---\n\n${renderedBody}\n`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function renderOrganisationBlueprintReceipt(resolved, approval) {
|
|
577
|
+
const lines = [
|
|
578
|
+
'# Organisation blueprint receipt',
|
|
579
|
+
'',
|
|
580
|
+
`- **Root blueprint:** ${resolved.root.name} (\`${resolved.root.id}\` ${resolved.root.version})`,
|
|
581
|
+
`- **Selection digest:** \`${resolved.digest}\``,
|
|
582
|
+
`- **Approved by:** ${approval.approvedBy || 'Pending named approval'}`,
|
|
583
|
+
`- **Approved at:** ${approval.approvedAt || 'Pending named approval'}`,
|
|
584
|
+
'',
|
|
585
|
+
'## Resolved packs',
|
|
586
|
+
'',
|
|
587
|
+
'| Pack | Version | Content digest |',
|
|
588
|
+
'| --- | --- | --- |',
|
|
589
|
+
...resolved.packs.map((pack) => `| \`${pack.id}\` | ${pack.version} | \`${pack.digest}\` |`),
|
|
590
|
+
'',
|
|
591
|
+
'## Applied modules',
|
|
592
|
+
'',
|
|
593
|
+
...resolved.modules.map(({ packId, module }) => `- \`${packId}:${module.id}\` — ${module.name}${module.required ? ' (required)' : ' (optional)'}`),
|
|
594
|
+
'',
|
|
595
|
+
'## Accepted Governed Starter Packs',
|
|
596
|
+
''
|
|
597
|
+
];
|
|
598
|
+
const starterPacks = resolved.modules.flatMap(({ packId, module }) => module.starterPacks.map((item) => ({ packId, moduleId: module.id, ...item })));
|
|
599
|
+
if (!starterPacks.length) lines.push('- None.');
|
|
600
|
+
else for (const item of starterPacks) {
|
|
601
|
+
const targets = item.targets.map((target) => `${target.role} from ${target.sourcePath}`).join(', ');
|
|
602
|
+
lines.push(`- **${item.name}** (\`${item.packId}:${item.moduleId}:${item.id}\`) — ${item.source} at ${item.version}; ${item.digest}; ${item.licence}; ${item.compatibility}; targets: ${targets}.${item.legacy ? ' Legacy boilerplate declaration normalised as a Governed Starter Pack.' : ''}`);
|
|
603
|
+
}
|
|
604
|
+
lines.push(
|
|
605
|
+
'',
|
|
606
|
+
'> Governed Starter Packs are accepted inert receipts only. Discovery did not fetch, clone, scaffold, execute, materialise, install, or deploy them.',
|
|
607
|
+
''
|
|
608
|
+
);
|
|
609
|
+
return lines.join('\n');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function organisationBlueprintOutputs(resolved, approval = {}) {
|
|
613
|
+
if (!resolved) return [];
|
|
614
|
+
const outputs = [];
|
|
615
|
+
for (const { packId, module } of resolved.modules) {
|
|
616
|
+
const pack = resolved.packs.find((candidate) => candidate.id === packId);
|
|
617
|
+
const publisher = pack.blueprint.publisher.id;
|
|
618
|
+
const packSlug = organisationPackSlug(pack);
|
|
619
|
+
for (const standard of module.standards) {
|
|
620
|
+
outputs.push({
|
|
621
|
+
kind: 'organisation-standard',
|
|
622
|
+
relative: `4.Constraints/standards/organisation/${publisher}/${packSlug}/${module.id}/${standard.id}.md`,
|
|
623
|
+
content: renderOrganisationStandard(pack, module, standard, approval)
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
for (const persona of module.personas) {
|
|
627
|
+
outputs.push({
|
|
628
|
+
kind: 'project-persona',
|
|
629
|
+
relative: `1.Scope/personas/project/${publisher}-${persona.id}.md`,
|
|
630
|
+
content: renderOrganisationPersona(pack, persona, approval)
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
outputs.push({
|
|
635
|
+
kind: 'organisation-blueprint-receipt',
|
|
636
|
+
relative: '5.Strategy/organisation-blueprint.md',
|
|
637
|
+
content: renderOrganisationBlueprintReceipt(resolved, approval)
|
|
638
|
+
});
|
|
639
|
+
const duplicate = outputs.find((output, index) => outputs.findIndex((candidate) => candidate.relative === output.relative) !== index);
|
|
640
|
+
if (duplicate) throw new Error(`Organisation blueprint output collision: ${duplicate.relative}`);
|
|
641
|
+
return outputs;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function resolveSelectedOrganisationBlueprint(projectRoot, selection, options = {}) {
|
|
645
|
+
if (!selection) return null;
|
|
646
|
+
const catalogue = options.organisationBlueprints ?? listOrganisationBlueprints({
|
|
647
|
+
projectRoot,
|
|
648
|
+
roots: options.organisationBlueprintRoots,
|
|
649
|
+
home: options.home
|
|
650
|
+
});
|
|
651
|
+
return resolveOrganisationBlueprint(selection.packId, catalogue, { enabledModules: selection.enabledModules });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function minimumStandards(answers) {
|
|
655
|
+
const standards = [
|
|
656
|
+
{
|
|
657
|
+
id: 'intent-and-acceptance',
|
|
658
|
+
title: 'Intent and acceptance evidence',
|
|
659
|
+
reason: 'Every change needs a reviewable purpose, affected users, constraints, and testable acceptance criteria.',
|
|
660
|
+
minimum: 'Create an EWAI intent before implementation and preserve decisions and evidence in SPECS.'
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
id: 'version-control-review',
|
|
664
|
+
title: 'Version control and independent review',
|
|
665
|
+
reason: 'Changes need traceability and a second set of eyes proportionate to risk.',
|
|
666
|
+
minimum: 'Use protected version control, focused commits, peer review, and explicit approval for destructive operations.'
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
id: 'automated-quality-gates',
|
|
670
|
+
title: 'Automated quality gates',
|
|
671
|
+
reason: 'The selected stack must have repeatable evidence that changes compile, conform, and behave as intended.',
|
|
672
|
+
minimum: 'Define install, lint, type or static analysis, test, and build commands; run them in continuous integration.'
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
id: 'dependency-and-secrets',
|
|
676
|
+
title: 'Dependency and secret hygiene',
|
|
677
|
+
reason: 'Deployable systems inherit supply-chain risk and must not commit credentials.',
|
|
678
|
+
minimum: 'Commit lock files, scan dependencies, keep secrets outside source control, and document supported runtime versions.'
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
id: 'security-by-default',
|
|
682
|
+
title: 'Security by default',
|
|
683
|
+
reason: 'Security controls are cheaper and more reliable when designed with the system rather than added at delivery.',
|
|
684
|
+
minimum: 'Apply least privilege, validate untrusted input, protect sensitive output, log security-relevant events, and threat-model material boundaries.'
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
id: 'operations-and-recovery',
|
|
688
|
+
title: 'Operations and recovery',
|
|
689
|
+
reason: 'A deployable service needs observable behaviour and a tested route back from failure.',
|
|
690
|
+
minimum: 'Define health signals, structured logs, backup and restore expectations, incident ownership, and rollback procedures.'
|
|
691
|
+
}
|
|
692
|
+
];
|
|
693
|
+
|
|
694
|
+
if (yesOrUnknown(answers.assurance.authentication)) {
|
|
695
|
+
standards.push({
|
|
696
|
+
id: 'identity-and-authorization',
|
|
697
|
+
title: 'Identity and authorization',
|
|
698
|
+
reason: `Authentication was answered ${answers.assurance.authentication}.`,
|
|
699
|
+
minimum: 'Specify identity source, session or token lifecycle, authorization boundaries, privileged actions, and access-control tests.'
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (yes(answers.assurance.multiTenant)) {
|
|
703
|
+
standards.push({
|
|
704
|
+
id: 'tenant-isolation',
|
|
705
|
+
title: 'Tenant isolation',
|
|
706
|
+
reason: 'The system will hold data or operations for more than one customer or organisation.',
|
|
707
|
+
minimum: 'Carry tenant context through every data path and test both permitted same-tenant access and blocked cross-tenant access.'
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
if (yesOrUnknown(answers.assurance.accessibility)) {
|
|
711
|
+
standards.push({
|
|
712
|
+
id: 'accessibility',
|
|
713
|
+
title: 'Accessibility',
|
|
714
|
+
reason: `Accessibility obligations were answered ${answers.assurance.accessibility}.`,
|
|
715
|
+
minimum: 'Set an accessibility target, use semantic interfaces, support keyboard operation, and include automated and human accessibility checks.'
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
if (['high', 'critical'].includes(answers.assurance.availability)) {
|
|
719
|
+
standards.push({
|
|
720
|
+
id: 'service-resilience',
|
|
721
|
+
title: 'Service resilience',
|
|
722
|
+
reason: `Availability was classified as ${answers.assurance.availability}.`,
|
|
723
|
+
minimum: 'Define service objectives, failure modes, redundancy, recovery time and recovery point objectives, capacity tests, and an exercised incident plan.'
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
if (yes(answers.assurance.aiFeatures)) {
|
|
727
|
+
standards.push({
|
|
728
|
+
id: 'responsible-ai-engineering',
|
|
729
|
+
title: 'Responsible AI engineering',
|
|
730
|
+
reason: 'The project includes AI-assisted or automated decisions or content.',
|
|
731
|
+
minimum: 'Document model and data provenance, evaluation criteria, failure handling, human oversight, monitoring, and user transparency.'
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return standards;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function complianceRecommendations(answers) {
|
|
739
|
+
const recommendations = [];
|
|
740
|
+
const add = (id, title, trigger, review) => recommendations.push({ id, title, trigger, review });
|
|
741
|
+
|
|
742
|
+
if (answers.assurance.jurisdictions.length === 0) {
|
|
743
|
+
add(
|
|
744
|
+
'jurisdiction-discovery',
|
|
745
|
+
'Jurisdiction and market assessment',
|
|
746
|
+
'No operating or user jurisdictions were confirmed.',
|
|
747
|
+
'Identify where the organisation, users, infrastructure, and affected data are located before selecting binding requirements.'
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (yesOrUnknown(answers.assurance.personalData)) {
|
|
751
|
+
add(
|
|
752
|
+
'data-protection',
|
|
753
|
+
'Privacy and data-protection assessment',
|
|
754
|
+
`Personal-data processing was answered ${answers.assurance.personalData}.`,
|
|
755
|
+
'Confirm applicability with a qualified owner; document data categories, purposes, lawful grounds, minimisation, retention, rights handling, processors, transfers, and breach response.'
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (yes(answers.assurance.sensitiveData)) {
|
|
759
|
+
add(
|
|
760
|
+
'high-risk-data',
|
|
761
|
+
'High-risk data and impact assessment',
|
|
762
|
+
'Sensitive or specially protected information will be processed.',
|
|
763
|
+
'Determine whether a formal privacy, security, equality, or sector impact assessment is required before approving the design.'
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (yes(answers.assurance.payments)) {
|
|
767
|
+
add(
|
|
768
|
+
'payment-scope',
|
|
769
|
+
'Payment and card-data scope review',
|
|
770
|
+
'The project will accept or process payments.',
|
|
771
|
+
'Keep payment data out of the application where practical and have a qualified owner determine the applicable payment-security scope and evidence.'
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (yes(answers.assurance.aiFeatures)) {
|
|
775
|
+
add(
|
|
776
|
+
'ai-governance',
|
|
777
|
+
'AI governance and regulatory assessment',
|
|
778
|
+
'The project includes AI-assisted or automated behaviour.',
|
|
779
|
+
'Classify the AI use, affected people, decision impact, providers, training and input data, human oversight, transparency, evaluation, and applicable jurisdictional obligations.'
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (yesOrUnknown(answers.assurance.accessibility)) {
|
|
783
|
+
add(
|
|
784
|
+
'accessibility-obligations',
|
|
785
|
+
'Accessibility obligations review',
|
|
786
|
+
`Accessibility obligations were answered ${answers.assurance.accessibility}.`,
|
|
787
|
+
'Identify the applicable accessibility standard, contractual target, user needs, testing method, and evidence owner.'
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (['confidential', 'restricted'].includes(answers.assurance.dataClassification)) {
|
|
791
|
+
add(
|
|
792
|
+
'information-security',
|
|
793
|
+
'Information-security control assessment',
|
|
794
|
+
`The highest expected data classification is ${answers.assurance.dataClassification}.`,
|
|
795
|
+
'Select a proportionate security control baseline and define encryption, access review, audit, retention, incident, supplier, and recovery evidence.'
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
for (const domain of answers.assurance.regulatedDomains) {
|
|
799
|
+
add(
|
|
800
|
+
`sector-${domain.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')}`,
|
|
801
|
+
`${domain} obligations review`,
|
|
802
|
+
`${domain} was identified as a regulated or contractually controlled domain.`,
|
|
803
|
+
'Assign a qualified owner to identify binding laws, regulator guidance, contracts, records, controls, testing, and approval evidence before Build.'
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
if (yes(answers.assurance.internetFacing)) {
|
|
807
|
+
add(
|
|
808
|
+
'internet-service-security',
|
|
809
|
+
'Internet-facing service security review',
|
|
810
|
+
'The project will expose a service to the public internet.',
|
|
811
|
+
'Define attack-surface ownership, secure configuration, vulnerability management, abuse controls, penetration-test expectations, monitoring, and incident response.'
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return recommendations;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function renderFrontmatter(schema, completedAt) {
|
|
819
|
+
return `---\nschema: ${schema}\nstatus: draft-for-review\ndiscovered_at: ${completedAt}\n---`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function renderContext(answers, completedAt) {
|
|
823
|
+
return `${renderFrontmatter('ewai.project-context/v1', completedAt)}\n\n# Project Context: ${answers.project.name}\n\nThis is the strategic entry point for agents and collaborators. Read the linked SPECS before planning or implementation.\n\n## Project in one sentence\n\n${answers.project.oneSentence}\n\n## Primary users\n\n${bullets(answers.project.primaryUsers)}\n\n## North Star\n\n${bullets(answers.project.desiredOutcomes)}\n\n## Non-negotiables\n\n${bullets(answers.project.nonNegotiables, 'None confirmed; review project constraints before Build')}\n\n## Technology direction\n\n- **Selected packs:** ${answers.delivery.technologyPacks.join(', ') || 'None selected'}\n- **Custom technology:** ${answers.delivery.customTechnology.join(', ') || 'None'}\n- **Deployment:** ${answers.delivery.deploymentTarget}\n\n## Canonical references\n\n- Scope: SPECS/1.Scope/project-scope.md\n- Intent: SPECS/2.Purpose/intent-brief.md\n- Success: SPECS/3.Evidence/success-criteria.md\n- Constraints: SPECS/4.Constraints/project-constraints.md\n- Approach: SPECS/5.Strategy/approach.md\n- Stack: SPECS/5.Strategy/architecture/stack.md\n`;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function renderProjectScope(answers, completedAt) {
|
|
827
|
+
const included = unique([...answers.project.inScope, ...answers.project.productTypes.map((type) => `${type} product surface`)]);
|
|
828
|
+
return `${renderFrontmatter('ewai.project-scope/v1', completedAt)}\n\n# Project Scope: ${answers.project.name}\n\n## What this project includes\n\n${bullets(included, 'Scope requires confirmation')}\n\n## Primary users and affected people\n\n${bullets(answers.project.primaryUsers)}\n\n## What this project excludes (non-goals)\n\n${bullets(answers.project.nonGoals, 'No explicit non-goals were confirmed')}\n\n## Do not touch\n\n${bullets(answers.project.dontTouch, 'No protected systems, code, or processes were confirmed')}\n\n## Project shape\n\n| Attribute | Discovery answer |\n|---|---|\n| Stage | ${escapeTable(answers.project.stage)} |\n| Product types | ${escapeTable(answers.project.productTypes.join(', '))} |\n| Deployment target | ${escapeTable(answers.delivery.deploymentTarget)} |\n| Availability | ${escapeTable(answers.assurance.availability)} |\n`;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function renderIntentBrief(answers, completedAt) {
|
|
832
|
+
return `${renderFrontmatter('ewai.project-intent/v1', completedAt)}\n\n# Project Intent: ${answers.project.name}\n\n## The problem\n\n${answers.project.problem}\n\n## The people experiencing it\n\n${bullets(answers.project.primaryUsers)}\n\n## The North Star\n\n${answers.project.purpose}\n\n${bullets(answers.project.desiredOutcomes)}\n\n## Why now\n\n${answers.project.whyNow}\n\n## Assumed capabilities\n\n${bullets(answers.project.capabilities, 'No capabilities were confirmed during initial discovery')}\n\n## Capabilities surfaced through challenge\n\n${bullets(answers.project.missingCapabilities, 'No additional capabilities were surfaced')}\n\n## Ownership\n\n| Role | Owner | Accountability |\n|---|---|---|\n| Product | ${escapeTable(answers.project.ownership.product)} | Success or failure of outcomes |\n| Delivery | ${escapeTable(answers.project.ownership.delivery)} | Execution and quality |\n| Technical | ${escapeTable(answers.project.ownership.technical)} | Architecture and constraints |\n`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function renderDiscoveryEvidence(answers, detected, standards, compliance, completedAt) {
|
|
836
|
+
const rows = [
|
|
837
|
+
['Project stage', answers.project.stage],
|
|
838
|
+
['Product types', answers.project.productTypes.join(', ')],
|
|
839
|
+
['Detected technology packs', detected.join(', ') || 'None'],
|
|
840
|
+
['Selected technology packs', answers.delivery.technologyPacks.join(', ') || 'None'],
|
|
841
|
+
['Custom technology', answers.delivery.customTechnology.join(', ') || 'None'],
|
|
842
|
+
['Deployment target', answers.delivery.deploymentTarget],
|
|
843
|
+
['Jurisdictions', answers.assurance.jurisdictions.join(', ') || 'Unknown'],
|
|
844
|
+
['Regulated domains', answers.assurance.regulatedDomains.join(', ') || 'None confirmed'],
|
|
845
|
+
['Data classification', answers.assurance.dataClassification],
|
|
846
|
+
['Personal data', answers.assurance.personalData],
|
|
847
|
+
['Sensitive data', answers.assurance.sensitiveData],
|
|
848
|
+
['Authentication', answers.assurance.authentication],
|
|
849
|
+
['Multi-tenant', answers.assurance.multiTenant],
|
|
850
|
+
['Internet-facing', answers.assurance.internetFacing],
|
|
851
|
+
['Payments', answers.assurance.payments],
|
|
852
|
+
['AI features', answers.assurance.aiFeatures],
|
|
853
|
+
['Accessibility obligations', answers.assurance.accessibility],
|
|
854
|
+
['Availability', answers.assurance.availability],
|
|
855
|
+
...VALIDATION_PROVIDERS.map((provider) => [
|
|
856
|
+
`${provider[0].toUpperCase()}${provider.slice(1)} external validation`,
|
|
857
|
+
`${answers.validation.external.providers[provider].state}; ${
|
|
858
|
+
answers.validation.external.providers[provider].enabled ? 'enabled' : 'disabled'
|
|
859
|
+
}`,
|
|
860
|
+
]),
|
|
861
|
+
...Object.keys(VALIDATION_CHECKPOINTS).map((checkpoint) => {
|
|
862
|
+
const setting = answers.validation.external.checkpoints[checkpoint];
|
|
863
|
+
return [
|
|
864
|
+
`${checkpoint} validation policy`,
|
|
865
|
+
`${setting.max_cycles} cycle(s); ${setting.review.breadth}; ${setting.review.depth}; ${setting.review.output} output`,
|
|
866
|
+
];
|
|
867
|
+
}),
|
|
868
|
+
];
|
|
869
|
+
return `${renderFrontmatter('ewai.project-discovery/v1', completedAt)}\n\n# Project Discovery Exploration\n\nThis file records the answers used to derive the initial project SPECS. Recommendations are planning evidence, not proof of legal or regulatory compliance.\n\n## Interview record\n\n| Question area | Answer |\n|---|---|\n${rows.map(([label, value]) => `| ${label} | ${escapeTable(value)} |`).join('\n')}\n\n## Derived output\n\n- ${standards.length} minimum engineering standards recommended.\n- ${compliance.length} candidate compliance or assurance reviews identified.\n- ${answers.delivery.technologyPacks.length || answers.delivery.customTechnology.length ? 'A technology direction was selected.' : 'Technology selection remains open.'}\n\n## Review status\n\n- [ ] Product owner confirms purpose, users, outcomes, and non-goals.\n- [ ] Technical owner confirms stack and deployment direction.\n- [ ] Security or compliance owner confirms which candidate reviews are applicable.\n`;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function renderSuccessCriteria(answers, completedAt) {
|
|
873
|
+
return `${renderFrontmatter('ewai.success-criteria/v1', completedAt)}\n\n# Project Success Criteria\n\n## Success signals\n\n${bullets(answers.project.successSignals, 'Success signals require confirmation')}\n\n## Failure signals\n\n${bullets(answers.project.failureSignals, 'Failure signals require confirmation')}\n\n## Definition of Ready\n\n${bullets(answers.project.definitionOfReady, 'Purpose, scope, constraints, ownership, and acceptance evidence are confirmed')}\n\n## Definition of Done\n\n${bullets(answers.project.definitionOfDone, 'Accepted outcomes are evidenced, delivery gates pass, manual QA is approved, and Retro is complete')}\n`;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function renderProjectConstraints(answers, completedAt) {
|
|
877
|
+
const hard = unique([...answers.project.hardConstraints, ...answers.delivery.constraints]);
|
|
878
|
+
return `${renderFrontmatter('ewai.project-constraints/v1', completedAt)}\n\n# Project Constraints\n\nThese constraints were stated during discovery. They require owner review before their status becomes binding for inherited feature SPECS.\n\n## Hard constraints (facts)\n\n${bullets(hard, 'No hard constraints were confirmed')}\n\n## Soft constraints (assumptions to test)\n\n${bullets(answers.project.softConstraints, 'No soft constraints were recorded')}\n\n## Non-negotiables\n\n${bullets(answers.project.nonNegotiables, 'No project-specific non-negotiables were confirmed')}\n\n## Compliance constraints\n\nCandidate obligations remain in SPECS/3.Evidence/risk/compliance-applicability.md until a qualified owner confirms and promotes them into SPECS/4.Constraints/compliance/.\n`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function renderApproach(answers, completedAt) {
|
|
882
|
+
const providerLines = VALIDATION_PROVIDERS.map((provider) => {
|
|
883
|
+
const setting = answers.validation.external.providers[provider];
|
|
884
|
+
return `- **${provider[0].toUpperCase()}${provider.slice(1)}:** ${setting.state}; ${setting.enabled ? 'enabled' : 'disabled'}`;
|
|
885
|
+
}).join('\n');
|
|
886
|
+
const checkpointLines = Object.keys(VALIDATION_CHECKPOINTS).map((checkpoint) => {
|
|
887
|
+
const setting = answers.validation.external.checkpoints[checkpoint];
|
|
888
|
+
const validators = setting.validators === 'auto' ? 'automatic' : setting.validators.join(', ') || 'none';
|
|
889
|
+
return `- **${checkpoint}:** up to ${setting.max_cycles} cycle(s); validators ${validators}; ${setting.review.breadth} breadth; ${setting.review.depth}; ${setting.review.output} output`;
|
|
890
|
+
}).join('\n');
|
|
891
|
+
return `${renderFrontmatter('ewai.project-approach/v1', completedAt)}\n\n# Project Approach\n\n## Core capabilities required\n\n${bullets(unique([...answers.project.capabilities, ...answers.project.missingCapabilities]), 'Capabilities require feature discovery')}\n\n## Technology approach\n\n- **Packs:** ${answers.delivery.technologyPacks.join(', ') || 'Undecided'}\n- **Custom technology:** ${answers.delivery.customTechnology.join(', ') || 'None'}\n- **Deployment:** ${answers.delivery.deploymentTarget}\n\n## Validation approach\n\n### Available external systems\n\n${providerLines}\n\n### Verification and fix cycles\n\n${checkpointLines}\n\n- The active orchestrator is excluded from independent external validation.\n- Unsupported external stages remain visible and are marked not-supported; they are never recorded as passed.\n- Standards compliance is mandatory at every delivery, even when no external validator is available.\n\n## Open questions\n\n${bullets(answers.project.openQuestions, 'No open questions were recorded')}\n\n## Assumptions and risks\n\n${bullets(answers.project.risks, 'Risks require further discovery')}\n\n## Recommended next step\n\nReview these Project SPECS, resolve blocking questions, then use feature discovery to create the first intent and Feature SPECS. Do not begin Build from this project-level document alone.\n\n## Guardrail\n\nThe project must not become: ${answers.project.avoidBecoming || 'an implementation detached from the confirmed problem and outcomes'}.\n`;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function renderMinimumStandards(standards, completedAt) {
|
|
895
|
+
return `${renderFrontmatter('ewai.minimum-standards-options/v1', completedAt)}\n\n# Minimum Engineering Standards: Recommendation\n\nThese are proposed minimums derived from project discovery. They remain strategy options until a technical owner accepts them into SPECS/4.Constraints/standards.md.\n\n${standards.map((standard, index) => `## ${index + 1}. ${standard.title}\n\n**Why selected:** ${standard.reason}\n\n**Minimum:** ${standard.minimum}\n`).join('\n')}\n## Approval\n\n- [ ] Technical owner has accepted, replaced, or rejected each recommendation.\n- [ ] Accepted standards have been promoted into SPECS/4.Constraints/standards.md.\n- [ ] Exceptions have a reason, owner, compensating control, and review date.\n`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function renderComplianceProfile(recommendations, answers, completedAt) {
|
|
899
|
+
const content = recommendations.length
|
|
900
|
+
? recommendations.map((item, index) => `## ${index + 1}. ${item.title}\n\n- **Status:** Review required\n- **Trigger:** ${item.trigger}\n- **Recommended review:** ${item.review}\n- **Owner:** Unassigned\n- **Decision:** Open\n`).join('\n')
|
|
901
|
+
: 'No specific compliance review was triggered by the confirmed answers. This is not evidence that no obligations apply; reassess when jurisdictions, users, data, suppliers, or functionality change.\n';
|
|
902
|
+
|
|
903
|
+
return `${renderFrontmatter('ewai.compliance-applicability/v1', completedAt)}\n\n# Compliance Applicability and Assurance Risk\n\n> This is an early triage record, not legal advice, certification, or a declaration of compliance. A qualified owner must determine which requirements apply.\n\n## Context\n\n- **Jurisdictions supplied:** ${answers.assurance.jurisdictions.join(', ') || 'None'}\n- **Regulated domains supplied:** ${answers.assurance.regulatedDomains.join(', ') || 'None'}\n- **Highest data classification:** ${answers.assurance.dataClassification}\n\n${content}\n## Promotion rule\n\nConfirmed obligations must be promoted into individual entries under SPECS/4.Constraints/compliance/ with an authoritative source, owner, scope, enforcement mechanism, and review date.\n\n## Change triggers\n\nRepeat discovery when the project enters a new market, adds a new category of user or data, changes deployment location, introduces AI or payments, or materially changes suppliers or decision impact.\n`;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function renderTechnologyStrategy(answers, selectedPacks, completedAt) {
|
|
907
|
+
const packSections = selectedPacks.length
|
|
908
|
+
? selectedPacks.map((pack) => {
|
|
909
|
+
const commands = Object.entries(pack.commands ?? {});
|
|
910
|
+
return `### ${pack.name} (${pack.id})\n\n${pack.description}\n\n${commands.length ? `Recommended pack commands:\n\n${commands.map(([name, command]) => `- **${name}:** \`${command}\``).join('\n')}` : 'This pack composes or supplements its required technology packs.'}\n`;
|
|
911
|
+
}).join('\n')
|
|
912
|
+
: 'No EWAI technology pack has been selected. Record the decision criteria and evaluate a small proof before Build.\n';
|
|
913
|
+
|
|
914
|
+
return `${renderFrontmatter('ewai.technology-stack/v1', completedAt)}\n\n# Technology Stack Reference\n\n## Selection\n\n${packSections}\n${answers.delivery.customTechnology.length ? `## Custom technology\n\n${bullets(answers.delivery.customTechnology)}\n` : ''}\n## Deployment direction\n\n${answers.delivery.deploymentTarget}\n\n## Detection evidence\n\n${bullets(answers.delivery.detectedPacks, 'No installed technology pack was detected in the current repository')}\n\n## Decision checks\n\n- [ ] The stack supports the required product shape and deployment target.\n- [ ] Runtime and framework versions will be pinned.\n- [ ] Install, lint, analysis, test, build, migration, and deployment commands are defined.\n- [ ] Security support windows and dependency ownership are understood.\n- [ ] The team can operate and recover the selected stack.\n- [ ] Any recommended boilerplate has reviewed provenance, licence, version, and compatibility.\n`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function buildOutputs(answers, selectedPacks, completedAt) {
|
|
918
|
+
const standards = minimumStandards(answers);
|
|
919
|
+
const compliance = complianceRecommendations(answers);
|
|
920
|
+
const outputs = {
|
|
921
|
+
'1.Scope/context.md': renderContext(answers, completedAt),
|
|
922
|
+
'1.Scope/project-scope.md': renderProjectScope(answers, completedAt),
|
|
923
|
+
'2.Purpose/intent-brief.md': renderIntentBrief(answers, completedAt),
|
|
924
|
+
'2.Purpose/explorations/project-discovery.md': renderDiscoveryEvidence(
|
|
925
|
+
answers,
|
|
926
|
+
answers.delivery.detectedPacks,
|
|
927
|
+
standards,
|
|
928
|
+
compliance,
|
|
929
|
+
completedAt
|
|
930
|
+
),
|
|
931
|
+
'3.Evidence/success-criteria.md': renderSuccessCriteria(answers, completedAt),
|
|
932
|
+
'3.Evidence/risk/compliance-applicability.md': renderComplianceProfile(compliance, answers, completedAt),
|
|
933
|
+
'4.Constraints/project-constraints.md': renderProjectConstraints(answers, completedAt),
|
|
934
|
+
'5.Strategy/approach.md': renderApproach(answers, completedAt),
|
|
935
|
+
'5.Strategy/architecture/stack.md': renderTechnologyStrategy(answers, selectedPacks, completedAt),
|
|
936
|
+
'5.Strategy/options/minimum-standards.md': renderMinimumStandards(standards, completedAt)
|
|
937
|
+
};
|
|
938
|
+
return { outputs, standards, compliance };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export function loadDiscoveryAnswers(path) {
|
|
942
|
+
const absolute = resolve(path);
|
|
943
|
+
if (!existsSync(absolute)) throw new Error(`Discovery answers file does not exist: ${absolute}`);
|
|
944
|
+
const content = readFileSync(absolute, 'utf8');
|
|
945
|
+
if (!['.json', '.yaml', '.yml'].includes(extname(absolute).toLowerCase())) {
|
|
946
|
+
throw new Error('Discovery answers must be JSON or YAML');
|
|
947
|
+
}
|
|
948
|
+
return YAML.parse(content);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function prepareProjectDiscovery(projectRoot, rawAnswers, options = {}) {
|
|
952
|
+
const { config, paths } = loadProjectConfig(projectRoot);
|
|
953
|
+
const packs = options.packs ?? listPacks();
|
|
954
|
+
const detected = detectTechnologyPacks(paths.projectRoot, packs);
|
|
955
|
+
const answers = normaliseDiscoveryAnswers(rawAnswers, config, detected);
|
|
956
|
+
answers.delivery.technologyPacks = resolveSelectedPacks(answers.delivery.technologyPacks, packs);
|
|
957
|
+
|
|
958
|
+
const selectedById = new Map(packs.map((pack) => [pack.id, pack]));
|
|
959
|
+
const selectedPacks = answers.delivery.technologyPacks.map((id) => selectedById.get(id));
|
|
960
|
+
const completedAt = options.now ?? new Date().toISOString();
|
|
961
|
+
const { outputs, standards, compliance } = buildOutputs(answers, selectedPacks, completedAt);
|
|
962
|
+
const resolvedBlueprint = resolveSelectedOrganisationBlueprint(paths.projectRoot, answers.delivery.organisationBlueprint, options);
|
|
963
|
+
const blueprintOutputs = organisationBlueprintOutputs(resolvedBlueprint);
|
|
964
|
+
const preparedOutputs = [...Object.entries(outputs).map(([relative, content]) => ({ relative, content })), ...blueprintOutputs].map(({ relative, content, kind = 'discovery' }) => {
|
|
965
|
+
const path = safeSpecsDestination(paths.specsRoot, relative);
|
|
966
|
+
return { kind, relative, path, content: `${content.trimEnd()}\n`, exists: existsSync(path) };
|
|
967
|
+
});
|
|
968
|
+
const organisationBlueprint = resolvedBlueprint
|
|
969
|
+
? { ...projectOrganisationBlueprint(resolvedBlueprint), destinations: blueprintOutputs.map((output) => output.relative) }
|
|
970
|
+
: null;
|
|
971
|
+
|
|
972
|
+
return {
|
|
973
|
+
schema: 'ewai.prepared-project-discovery/v1',
|
|
974
|
+
projectRoot: paths.projectRoot,
|
|
975
|
+
specsRoot: paths.specsRoot,
|
|
976
|
+
preparedAt: completedAt,
|
|
977
|
+
answers,
|
|
978
|
+
outputs: preparedOutputs,
|
|
979
|
+
conflicts: preparedOutputs.filter((output) => output.exists).map((output) => output.relative),
|
|
980
|
+
selectedPacks: answers.delivery.technologyPacks,
|
|
981
|
+
detectedPacks: answers.delivery.detectedPacks,
|
|
982
|
+
organisationBlueprint,
|
|
983
|
+
minimumStandards: standards.length,
|
|
984
|
+
complianceReviews: compliance.length
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export function commitProjectDiscovery(projectRoot, prepared, options = {}) {
|
|
989
|
+
if (prepared?.schema !== 'ewai.prepared-project-discovery/v1') {
|
|
990
|
+
throw new Error('A prepared project discovery is required before commit');
|
|
991
|
+
}
|
|
992
|
+
const { config, paths } = loadProjectConfig(projectRoot);
|
|
993
|
+
if (resolve(prepared.projectRoot) !== paths.projectRoot) {
|
|
994
|
+
throw new Error('Prepared discovery belongs to a different project');
|
|
995
|
+
}
|
|
996
|
+
const expectedDiscovery = new Set(outputFiles);
|
|
997
|
+
const preparedRelatives = prepared.outputs.map((output) => output.relative);
|
|
998
|
+
const preparedDiscoveryRelatives = prepared.outputs.filter((output) => expectedDiscovery.has(output.relative)).map((output) => output.relative);
|
|
999
|
+
if (
|
|
1000
|
+
preparedDiscoveryRelatives.length !== expectedDiscovery.size
|
|
1001
|
+
|| new Set(preparedDiscoveryRelatives).size !== expectedDiscovery.size
|
|
1002
|
+
|| preparedDiscoveryRelatives.some((relative) => !expectedDiscovery.has(relative))
|
|
1003
|
+
) {
|
|
1004
|
+
throw new Error('Prepared discovery output contract is incomplete or invalid');
|
|
1005
|
+
}
|
|
1006
|
+
const answers = prepared.answers;
|
|
1007
|
+
const approvedAt = options.approvedAt ?? prepared.preparedAt ?? new Date().toISOString();
|
|
1008
|
+
const approvedBy = asString(options.approvedBy);
|
|
1009
|
+
const resolvedBlueprint = resolveSelectedOrganisationBlueprint(paths.projectRoot, answers.delivery.organisationBlueprint, options);
|
|
1010
|
+
if (resolvedBlueprint && !approvedBy) {
|
|
1011
|
+
throw new Error('Organisation blueprint approval requires an accountable approver name');
|
|
1012
|
+
}
|
|
1013
|
+
if (Boolean(resolvedBlueprint) !== Boolean(prepared.organisationBlueprint)) {
|
|
1014
|
+
throw new Error('Prepared discovery organisation blueprint contract is incomplete or invalid');
|
|
1015
|
+
}
|
|
1016
|
+
if (resolvedBlueprint && resolvedBlueprint.digest !== prepared.organisationBlueprint.digest) {
|
|
1017
|
+
throw new Error('Organisation blueprint changed since preview; review the new digest before approval');
|
|
1018
|
+
}
|
|
1019
|
+
const currentBlueprintOutputs = organisationBlueprintOutputs(resolvedBlueprint, { approvedBy, approvedAt });
|
|
1020
|
+
const expected = new Set([...outputFiles, ...currentBlueprintOutputs.map((output) => output.relative)]);
|
|
1021
|
+
if (
|
|
1022
|
+
preparedRelatives.length !== expected.size
|
|
1023
|
+
|| new Set(preparedRelatives).size !== expected.size
|
|
1024
|
+
|| preparedRelatives.some((relative) => !expected.has(relative))
|
|
1025
|
+
) {
|
|
1026
|
+
throw new Error('Prepared discovery output contract is incomplete or invalid');
|
|
1027
|
+
}
|
|
1028
|
+
const currentBlueprintByRelative = new Map(currentBlueprintOutputs.map((output) => [output.relative, output]));
|
|
1029
|
+
const destinations = prepared.outputs.map((output) => ({
|
|
1030
|
+
...output,
|
|
1031
|
+
...(currentBlueprintByRelative.get(output.relative) ?? {}),
|
|
1032
|
+
path: safeSpecsDestination(paths.specsRoot, output.relative)
|
|
1033
|
+
}));
|
|
1034
|
+
const existing = destinations.filter((output) => existsSync(output.path));
|
|
1035
|
+
if (existing.length && !options.force) {
|
|
1036
|
+
throw new Error(`Discovery outputs already exist; review them or use --force:\n${existing.map((output) => output.path).join('\n')}`);
|
|
1037
|
+
}
|
|
1038
|
+
const technologyIds = new Set(
|
|
1039
|
+
(options.packs ?? listPacks()).filter((pack) => ['technology', 'stack'].includes(pack.type)).map((pack) => pack.id)
|
|
1040
|
+
);
|
|
1041
|
+
const preservedPacks = (config.packs ?? []).filter((id) => !technologyIds.has(id));
|
|
1042
|
+
config.project = {
|
|
1043
|
+
...(config.project ?? {}),
|
|
1044
|
+
name: answers.project.name,
|
|
1045
|
+
discovery: {
|
|
1046
|
+
status: 'complete',
|
|
1047
|
+
completed_at: approvedAt,
|
|
1048
|
+
...(approvedBy ? { approved_by: approvedBy, approved_at: approvedAt } : {}),
|
|
1049
|
+
evidence: 'SPECS/2.Purpose/explorations/project-discovery.md'
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
config.packs = unique([
|
|
1053
|
+
...preservedPacks,
|
|
1054
|
+
...answers.delivery.technologyPacks,
|
|
1055
|
+
...(resolvedBlueprint?.packs.map((pack) => pack.id) ?? [])
|
|
1056
|
+
]);
|
|
1057
|
+
config.discovery = {
|
|
1058
|
+
answers_schema: ANSWERS_SCHEMA,
|
|
1059
|
+
project_scope: 'SPECS/1.Scope/project-scope.md',
|
|
1060
|
+
project_intent: 'SPECS/2.Purpose/intent-brief.md',
|
|
1061
|
+
success_criteria: 'SPECS/3.Evidence/success-criteria.md',
|
|
1062
|
+
technology_strategy: 'SPECS/5.Strategy/architecture/stack.md',
|
|
1063
|
+
minimum_standards_options: 'SPECS/5.Strategy/options/minimum-standards.md',
|
|
1064
|
+
compliance_risk: 'SPECS/3.Evidence/risk/compliance-applicability.md'
|
|
1065
|
+
};
|
|
1066
|
+
config.validation = answers.validation;
|
|
1067
|
+
if (resolvedBlueprint) {
|
|
1068
|
+
config.blueprints = {
|
|
1069
|
+
...(config.blueprints ?? {}),
|
|
1070
|
+
organisation: {
|
|
1071
|
+
root: {
|
|
1072
|
+
id: resolvedBlueprint.root.id,
|
|
1073
|
+
version: resolvedBlueprint.root.version,
|
|
1074
|
+
digest: resolvedBlueprint.root.digest
|
|
1075
|
+
},
|
|
1076
|
+
packs: resolvedBlueprint.packs.map((pack) => ({ id: pack.id, version: pack.version, digest: pack.digest })),
|
|
1077
|
+
selection_digest: resolvedBlueprint.digest,
|
|
1078
|
+
enabled_modules: [...resolvedBlueprint.enabledModules],
|
|
1079
|
+
applied_modules: resolvedBlueprint.modules.map(({ packId, module }) => `${packId}:${module.id}`),
|
|
1080
|
+
approved_by: approvedBy,
|
|
1081
|
+
approved_at: approvedAt,
|
|
1082
|
+
evidence: 'SPECS/5.Strategy/organisation-blueprint.md'
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
const originalConfig = readFileSync(paths.configPath, 'utf8');
|
|
1087
|
+
const writer = options.writeFile ?? writeFileSync;
|
|
1088
|
+
const written = [];
|
|
1089
|
+
const previousOutputs = new Map(
|
|
1090
|
+
existing.map((output) => [output.path, readFileSync(output.path, 'utf8')])
|
|
1091
|
+
);
|
|
1092
|
+
let configAttempted = false;
|
|
1093
|
+
try {
|
|
1094
|
+
for (const output of destinations) {
|
|
1095
|
+
mkdirSync(dirname(output.path), { recursive: true });
|
|
1096
|
+
written.push(output.path);
|
|
1097
|
+
writer(output.path, output.content, 'utf8');
|
|
1098
|
+
}
|
|
1099
|
+
configAttempted = true;
|
|
1100
|
+
writer(paths.configPath, YAML.stringify(config, { lineWidth: 0 }), 'utf8');
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
for (const path of written) {
|
|
1103
|
+
if (previousOutputs.has(path)) {
|
|
1104
|
+
writeFileSync(path, previousOutputs.get(path), 'utf8');
|
|
1105
|
+
} else {
|
|
1106
|
+
rmSync(path, { force: true });
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (configAttempted) writeFileSync(paths.configPath, originalConfig, 'utf8');
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
publishLifecycleEventSafely(paths.projectRoot, 'ewai.project.discovery.completed', {
|
|
1114
|
+
sourceKey: `project.discovery.completed:${approvedAt}`,
|
|
1115
|
+
occurredAt: approvedAt,
|
|
1116
|
+
sourceRevision: approvedAt,
|
|
1117
|
+
facts: {
|
|
1118
|
+
status: 'complete',
|
|
1119
|
+
selectedPacks: prepared.selectedPacks,
|
|
1120
|
+
minimumStandards: prepared.minimumStandards,
|
|
1121
|
+
complianceReviews: prepared.complianceReviews,
|
|
1122
|
+
...(resolvedBlueprint ? { organisationBlueprintId: resolvedBlueprint.root.id } : {}),
|
|
1123
|
+
},
|
|
1124
|
+
evidence: ['SPECS/2.Purpose/explorations/project-discovery.md'],
|
|
1125
|
+
personas: options.personas ?? [],
|
|
1126
|
+
streamId: 'project',
|
|
1127
|
+
now: approvedAt,
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
return {
|
|
1131
|
+
projectRoot: paths.projectRoot,
|
|
1132
|
+
created: destinations.map((output) => output.path),
|
|
1133
|
+
selectedPacks: prepared.selectedPacks,
|
|
1134
|
+
detectedPacks: prepared.detectedPacks,
|
|
1135
|
+
minimumStandards: prepared.minimumStandards,
|
|
1136
|
+
complianceReviews: prepared.complianceReviews,
|
|
1137
|
+
approval: { approvedBy, approvedAt },
|
|
1138
|
+
organisationBlueprint: resolvedBlueprint ? projectOrganisationBlueprint(resolvedBlueprint) : null,
|
|
1139
|
+
nextCommand: 'ewai intent create <slug> --domain <domain> --title <title>'
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export function discoverProject(projectRoot, rawAnswers, options = {}) {
|
|
1144
|
+
const prepared = prepareProjectDiscovery(projectRoot, rawAnswers, options);
|
|
1145
|
+
return commitProjectDiscovery(projectRoot, prepared, {
|
|
1146
|
+
force: options.force,
|
|
1147
|
+
packs: options.packs,
|
|
1148
|
+
organisationBlueprints: options.organisationBlueprints,
|
|
1149
|
+
organisationBlueprintRoots: options.organisationBlueprintRoots,
|
|
1150
|
+
home: options.home,
|
|
1151
|
+
approvedBy: options.approvedBy,
|
|
1152
|
+
approvedAt: options.now,
|
|
1153
|
+
personas: options.personas,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async function askRequired(rl, question, fallback = '') {
|
|
1158
|
+
while (true) {
|
|
1159
|
+
const suffix = fallback ? ` [${fallback}]` : '';
|
|
1160
|
+
const answer = asString(await rl.question(`${question}${suffix}: `), fallback);
|
|
1161
|
+
if (answer) return answer;
|
|
1162
|
+
rl.write('Please provide an answer.\n');
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
async function askList(rl, question, fallback = []) {
|
|
1167
|
+
const defaultText = fallback.join(', ');
|
|
1168
|
+
const answer = asString(await rl.question(`${question}${defaultText ? ` [${defaultText}]` : ''}: `), defaultText);
|
|
1169
|
+
return asList(answer);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
async function askEnum(rl, question, allowed, fallback) {
|
|
1173
|
+
while (true) {
|
|
1174
|
+
const answer = asString(await rl.question(`${question} (${allowed.join('/')}) [${fallback}]: `), fallback).toLowerCase();
|
|
1175
|
+
if (allowed.includes(answer)) return answer;
|
|
1176
|
+
rl.write(`Choose one of: ${allowed.join(', ')}.\n`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
export async function runDiscoveryInterview(projectRoot, io = {}) {
|
|
1181
|
+
const { config, paths } = loadProjectConfig(projectRoot);
|
|
1182
|
+
const packs = listPacks();
|
|
1183
|
+
const detected = detectTechnologyPacks(paths.projectRoot, packs);
|
|
1184
|
+
const selectable = packs.filter((pack) => ['technology', 'stack'].includes(pack.type));
|
|
1185
|
+
const defaultStack = detected.find((id) => selectable.find((pack) => pack.id === id)?.type === 'stack')
|
|
1186
|
+
?? detected[0]
|
|
1187
|
+
?? 'undecided';
|
|
1188
|
+
const rl = createInterface({
|
|
1189
|
+
input: io.input ?? process.stdin,
|
|
1190
|
+
output: io.output ?? process.stdout,
|
|
1191
|
+
terminal: io.terminal ?? Boolean((io.output ?? process.stdout).isTTY)
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
try {
|
|
1195
|
+
rl.write('\nEWAI project discovery\n');
|
|
1196
|
+
rl.write('Answers become draft SPECS for review; compliance results are triage recommendations only.\n\n');
|
|
1197
|
+
if (detected.length) rl.write(`Detected technology packs: ${detected.join(', ')}\n`);
|
|
1198
|
+
|
|
1199
|
+
const name = await askRequired(rl, 'Project name', config.project?.name ?? basename(paths.projectRoot));
|
|
1200
|
+
const whyNow = await askRequired(rl, 'Why does this project exist now? What changed or became unacceptable?');
|
|
1201
|
+
const purpose = await askRequired(rl, 'What is the project for?');
|
|
1202
|
+
const problem = await askRequired(rl, 'What problem or unmet need does it address?');
|
|
1203
|
+
const primaryUsers = await askList(rl, 'Who are its primary users? (comma-separated)');
|
|
1204
|
+
if (!primaryUsers.length) throw new Error('At least one primary user is required');
|
|
1205
|
+
const desiredOutcomes = await askList(rl, 'What outcomes should it create? (comma-separated)');
|
|
1206
|
+
if (!desiredOutcomes.length) throw new Error('At least one desired outcome is required');
|
|
1207
|
+
const capabilities = await askList(rl, 'Capabilities you already believe are required (comma-separated)');
|
|
1208
|
+
const missingCapabilities = await askList(rl, 'What would disappointed users say it cannot do? (comma-separated, optional)');
|
|
1209
|
+
const stage = await askEnum(rl, 'Project stage', [...PROJECT_STAGES], 'new');
|
|
1210
|
+
const productTypes = await askList(rl, 'Product types (for example web application, API, CLI)');
|
|
1211
|
+
const inScope = await askList(rl, 'What is explicitly in scope? (comma-separated)');
|
|
1212
|
+
const nonGoals = await askList(rl, 'Known non-goals (comma-separated, optional)');
|
|
1213
|
+
const dontTouch = await askList(rl, 'Systems, code, or processes this must not touch (comma-separated, optional)');
|
|
1214
|
+
const hardConstraints = await askList(rl, 'Hard constraints that are facts (comma-separated, optional)');
|
|
1215
|
+
const softConstraints = await askList(rl, 'Soft constraints or inherited assumptions (comma-separated, optional)');
|
|
1216
|
+
const nonNegotiables = await askList(rl, 'Non-negotiable project rules (comma-separated, optional)');
|
|
1217
|
+
const successSignals = await askList(rl, 'Observable success signals (comma-separated)', desiredOutcomes);
|
|
1218
|
+
const failureSignals = await askList(rl, 'Observable signs the project is quietly failing (comma-separated, optional)');
|
|
1219
|
+
const definitionOfReady = await askList(rl, 'What must be true before Build can start? (comma-separated, optional)');
|
|
1220
|
+
const definitionOfDone = await askList(rl, 'What evidence is required to call the project done? (comma-separated, optional)');
|
|
1221
|
+
const productOwner = await askRequired(rl, 'Who owns product success or failure?', 'Unassigned');
|
|
1222
|
+
const deliveryOwner = await askRequired(rl, 'Who owns delivery and quality?', 'Unassigned');
|
|
1223
|
+
const technicalOwner = await askRequired(rl, 'Who owns architecture and technical constraints?', 'Unassigned');
|
|
1224
|
+
const openQuestions = await askList(rl, 'Open questions that block confident planning (comma-separated, optional)');
|
|
1225
|
+
const risks = await askList(rl, 'Important assumptions or failure risks (comma-separated, optional)');
|
|
1226
|
+
const oneSentence = await askRequired(rl, 'Describe this project honestly in one sentence', purpose);
|
|
1227
|
+
const avoidBecoming = await askRequired(rl, 'What is the one thing you do not want this project to become?', nonGoals[0] ?? 'A solution detached from the real problem');
|
|
1228
|
+
|
|
1229
|
+
rl.write('\nAvailable technology packs:\n');
|
|
1230
|
+
for (const pack of selectable) rl.write(`- ${pack.id}: ${pack.name}\n`);
|
|
1231
|
+
const stack = await askRequired(rl, 'Select a technology pack ID, custom, or undecided', defaultStack);
|
|
1232
|
+
const technologyPacks = ['custom', 'undecided'].includes(stack) ? [] : [stack];
|
|
1233
|
+
const customTechnology = stack === 'custom'
|
|
1234
|
+
? await askList(rl, 'Custom technologies (comma-separated)')
|
|
1235
|
+
: [];
|
|
1236
|
+
const deploymentTarget = await askRequired(rl, 'Expected deployment target', 'undecided');
|
|
1237
|
+
const constraints = await askList(rl, 'Technical or delivery constraints (comma-separated, optional)');
|
|
1238
|
+
|
|
1239
|
+
const jurisdictions = await askList(rl, 'Operating and user jurisdictions (comma-separated, unknown is acceptable)');
|
|
1240
|
+
const regulatedDomains = await askList(rl, 'Regulated or contract-controlled domains (comma-separated, optional)');
|
|
1241
|
+
const dataClassification = await askEnum(rl, 'Highest expected data classification', [...DATA_CLASSIFICATIONS], 'unknown');
|
|
1242
|
+
const personalData = await askEnum(rl, 'Will it process personal data?', [...TRI_STATE], 'unknown');
|
|
1243
|
+
const sensitiveData = await askEnum(rl, 'Will it process sensitive or specially protected data?', [...TRI_STATE], 'unknown');
|
|
1244
|
+
const authentication = await askEnum(rl, 'Will users authenticate?', [...TRI_STATE], 'unknown');
|
|
1245
|
+
const multiTenant = await askEnum(rl, 'Will it serve multiple customers or organisations?', [...TRI_STATE], 'unknown');
|
|
1246
|
+
const internetFacing = await askEnum(rl, 'Will any service be internet-facing?', [...TRI_STATE], 'unknown');
|
|
1247
|
+
const payments = await askEnum(rl, 'Will it accept or process payments?', [...TRI_STATE], 'unknown');
|
|
1248
|
+
const aiFeatures = await askEnum(rl, 'Will it use AI-assisted or automated behaviour?', [...TRI_STATE], 'unknown');
|
|
1249
|
+
const accessibility = await askEnum(rl, 'Are accessibility obligations expected?', [...TRI_STATE], 'unknown');
|
|
1250
|
+
const availability = await askEnum(rl, 'Availability expectation', [...AVAILABILITY_LEVELS], 'standard');
|
|
1251
|
+
const configuredProviders = config.validation?.external?.providers ?? {};
|
|
1252
|
+
const validationProviders = {};
|
|
1253
|
+
for (const provider of VALIDATION_PROVIDERS) {
|
|
1254
|
+
const label = `${provider[0].toUpperCase()}${provider.slice(1)}`;
|
|
1255
|
+
validationProviders[provider] = await askEnum(
|
|
1256
|
+
rl,
|
|
1257
|
+
`Is ${label} CLI available for external validation in this project?`,
|
|
1258
|
+
[...VALIDATOR_STATES],
|
|
1259
|
+
configuredProviders[provider]?.state ?? 'unavailable',
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return discoverProject(paths.projectRoot, {
|
|
1264
|
+
schema: ANSWERS_SCHEMA,
|
|
1265
|
+
project: {
|
|
1266
|
+
name,
|
|
1267
|
+
purpose,
|
|
1268
|
+
whyNow,
|
|
1269
|
+
problem,
|
|
1270
|
+
primaryUsers,
|
|
1271
|
+
desiredOutcomes,
|
|
1272
|
+
capabilities,
|
|
1273
|
+
missingCapabilities,
|
|
1274
|
+
stage,
|
|
1275
|
+
productTypes,
|
|
1276
|
+
inScope,
|
|
1277
|
+
nonGoals,
|
|
1278
|
+
dontTouch,
|
|
1279
|
+
hardConstraints,
|
|
1280
|
+
softConstraints,
|
|
1281
|
+
nonNegotiables,
|
|
1282
|
+
successSignals,
|
|
1283
|
+
failureSignals,
|
|
1284
|
+
definitionOfReady,
|
|
1285
|
+
definitionOfDone,
|
|
1286
|
+
openQuestions,
|
|
1287
|
+
risks,
|
|
1288
|
+
oneSentence,
|
|
1289
|
+
avoidBecoming,
|
|
1290
|
+
ownership: { product: productOwner, delivery: deliveryOwner, technical: technicalOwner }
|
|
1291
|
+
},
|
|
1292
|
+
delivery: { technologyPacks, customTechnology, deploymentTarget, constraints, detectedPacks: detected },
|
|
1293
|
+
assurance: {
|
|
1294
|
+
jurisdictions,
|
|
1295
|
+
regulatedDomains,
|
|
1296
|
+
dataClassification,
|
|
1297
|
+
personalData,
|
|
1298
|
+
sensitiveData,
|
|
1299
|
+
authentication,
|
|
1300
|
+
multiTenant,
|
|
1301
|
+
internetFacing,
|
|
1302
|
+
payments,
|
|
1303
|
+
aiFeatures,
|
|
1304
|
+
accessibility,
|
|
1305
|
+
availability
|
|
1306
|
+
},
|
|
1307
|
+
validation: {
|
|
1308
|
+
providers: validationProviders,
|
|
1309
|
+
}
|
|
1310
|
+
}, io);
|
|
1311
|
+
} finally {
|
|
1312
|
+
rl.close();
|
|
1313
|
+
}
|
|
1314
|
+
}
|