@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,620 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
statSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { deliveryPaths, sha256 } from './delivery-documents.mjs';
|
|
18
|
+
import { projectPaths } from './paths.mjs';
|
|
19
|
+
import { listPersonas, personalPersonaRoot, projectPersonaRoot } from './personas.mjs';
|
|
20
|
+
import { premiumPersonaRoot } from './checkin.mjs';
|
|
21
|
+
import { ASSURANCE_NOTICE } from './security-validation-config.mjs';
|
|
22
|
+
import { assessSecurityReadiness } from './runtime/security-validation.mjs';
|
|
23
|
+
import { readImpactWorkspace } from './runtime/impact-analysis.mjs';
|
|
24
|
+
import { selectContextualPersonas } from './runtime/persona-engagement.mjs';
|
|
25
|
+
import { readPersonaTestScenarioWorkspace } from './test-scenarios.mjs';
|
|
26
|
+
import { POLICY_DESIGN_AUTHORITY_NOTICE } from './policy-gate-integration.mjs';
|
|
27
|
+
|
|
28
|
+
export const READINESS_AUTHORITY_NOTICE = 'Solution Readiness Review is advisory evidence, not certification, business acceptance, Manual QA approval, security approval, deployment permission or release authorisation. Accountable humans retain every approval and risk decision.';
|
|
29
|
+
|
|
30
|
+
const BRIEF_SCHEMA = 'ewai.solution-readiness-brief/v1';
|
|
31
|
+
const REVIEW_INPUT_SCHEMA = 'ewai.solution-readiness-review-input/v1';
|
|
32
|
+
const REPORT_SCHEMA = 'ewai.solution-readiness-report/v1';
|
|
33
|
+
const ASSESSMENT_PATTERN = /^[a-f0-9]{16}$/;
|
|
34
|
+
const STATES = new Set(['satisfied', 'conditional', 'blocking', 'missing', 'stale', 'not-applicable', 'not-configured']);
|
|
35
|
+
const DISPOSITIONS = new Set(['accepted', 'conditional', 'blocked', 'insufficient-evidence', 'not-applicable']);
|
|
36
|
+
const DIMENSIONS = Object.freeze([
|
|
37
|
+
['purpose-acceptance', 'Purpose and acceptance'],
|
|
38
|
+
['impact', 'Impact and affected journeys'],
|
|
39
|
+
['standards', 'Standards coverage'],
|
|
40
|
+
['tests', 'Automated and persona-driven tests'],
|
|
41
|
+
['manual-qa', 'Manual QA'],
|
|
42
|
+
['external-validation', 'Independent external validation'],
|
|
43
|
+
['security', 'Security validation'],
|
|
44
|
+
['technology-hosting', 'Technology and hosting'],
|
|
45
|
+
['deployment-operations', 'Deployment and operations'],
|
|
46
|
+
['documentation-training', 'Documentation and training'],
|
|
47
|
+
['governance-specialist-assurance', 'Governance and specialist assurance'],
|
|
48
|
+
].map(([id, name]) => Object.freeze({ id, name })));
|
|
49
|
+
|
|
50
|
+
const profiles = [
|
|
51
|
+
{
|
|
52
|
+
id: 'internal-only',
|
|
53
|
+
name: 'Internal only',
|
|
54
|
+
description: 'A bounded internal tool without sensitive or client material.',
|
|
55
|
+
requiredDimensions: ['purpose-acceptance', 'standards', 'tests', 'manual-qa', 'deployment-operations'],
|
|
56
|
+
requiredHumanRoles: ['Product Owner', 'Technical Owner'],
|
|
57
|
+
signals: ['internal tool', 'acceptance', 'operations', 'testing'],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'internal-sensitive',
|
|
61
|
+
name: 'Internal sensitive',
|
|
62
|
+
description: 'An internal solution handling sensitive, personal or commercially restricted information.',
|
|
63
|
+
requiredDimensions: ['purpose-acceptance', 'standards', 'tests', 'manual-qa', 'security', 'technology-hosting', 'deployment-operations', 'governance-specialist-assurance'],
|
|
64
|
+
requiredHumanRoles: ['Product Owner', 'Technical Owner', 'Security or Privacy Owner'],
|
|
65
|
+
signals: ['sensitive data', 'security', 'privacy', 'hosting', 'operations'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'client-facing',
|
|
69
|
+
name: 'Client facing',
|
|
70
|
+
description: 'A solution used by clients or handling client workflows and material.',
|
|
71
|
+
requiredDimensions: ['purpose-acceptance', 'impact', 'standards', 'tests', 'manual-qa', 'external-validation', 'security', 'technology-hosting', 'deployment-operations', 'documentation-training', 'governance-specialist-assurance'],
|
|
72
|
+
requiredHumanRoles: ['Product Owner', 'Technical Owner', 'Security or Privacy Owner', 'Service Owner'],
|
|
73
|
+
signals: ['client', 'user journey', 'impact', 'security', 'service operations', 'training'],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'public-service',
|
|
77
|
+
name: 'Public service',
|
|
78
|
+
description: 'A publicly accessible service with broader availability, misuse and accessibility consequences.',
|
|
79
|
+
requiredDimensions: DIMENSIONS.map((item) => item.id),
|
|
80
|
+
requiredHumanRoles: ['Product Owner', 'Technical Owner', 'Security or Privacy Owner', 'Service Owner', 'Accessibility or User Representative'],
|
|
81
|
+
signals: ['public service', 'accessibility', 'misuse', 'security', 'operations', 'user impact'],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'critical-regulated',
|
|
85
|
+
name: 'Critical or regulated',
|
|
86
|
+
description: 'A high-consequence or regulated context requiring named specialist and independent assurance routes.',
|
|
87
|
+
requiredDimensions: DIMENSIONS.map((item) => item.id),
|
|
88
|
+
requiredHumanRoles: ['Product Owner', 'Technical Owner', 'Security or Privacy Owner', 'Service Owner', 'Legal or Regulatory Specialist', 'Independent Assurance Reviewer'],
|
|
89
|
+
signals: ['critical', 'regulated', 'independent assurance', 'legal', 'security', 'residual risk', 'operations'],
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
function deepFreeze(value) {
|
|
94
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
95
|
+
Object.freeze(value);
|
|
96
|
+
for (const item of Object.values(value)) deepFreeze(item);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
deepFreeze(profiles);
|
|
102
|
+
|
|
103
|
+
function canonical(value) {
|
|
104
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
105
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function digest(value) {
|
|
110
|
+
return sha256(JSON.stringify(canonical(value)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function clean(value, label, maximum = 2_000) {
|
|
114
|
+
const result = String(value ?? '').trim();
|
|
115
|
+
if (!result) throw new Error(`Solution readiness requires ${label}.`);
|
|
116
|
+
if (result.length > maximum) throw new Error(`Solution readiness ${label} is too long.`);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function exactFields(value, allowed, label) {
|
|
121
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
122
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
123
|
+
if (unknown.length) throw new Error(`${label} contains unknown field(s): ${unknown.join(', ')}.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function relativePath(projectRoot, path) {
|
|
127
|
+
return relative(resolve(projectRoot), resolve(path)).replaceAll('\\', '/');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isWithin(root, target) {
|
|
131
|
+
const rel = relative(resolve(root), resolve(target));
|
|
132
|
+
return rel === '' || (rel && !rel.startsWith('..') && !isAbsolute(rel));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function assertNoSymlinkEscape(root, target, label) {
|
|
136
|
+
const inputRoot = resolve(root);
|
|
137
|
+
const inputTarget = resolve(target);
|
|
138
|
+
if (!isWithin(inputRoot, inputTarget)) throw new Error(`${label} must remain inside the project.`);
|
|
139
|
+
const safeRoot = existsSync(inputRoot) ? realpathSync(inputRoot) : inputRoot;
|
|
140
|
+
const safeTarget = resolve(safeRoot, relative(inputRoot, inputTarget));
|
|
141
|
+
if (!isWithin(safeRoot, safeTarget)) throw new Error(`${label} must remain inside the project.`);
|
|
142
|
+
let cursor = safeRoot;
|
|
143
|
+
const rel = relative(safeRoot, safeTarget);
|
|
144
|
+
for (const part of rel.split(/[\\/]/).filter(Boolean)) {
|
|
145
|
+
cursor = resolve(cursor, part);
|
|
146
|
+
if (!existsSync(cursor)) break;
|
|
147
|
+
if (lstatSync(cursor).isSymbolicLink()) throw new Error(`${label} cannot traverse a symbolic link.`);
|
|
148
|
+
if (!isWithin(safeRoot, realpathSync(cursor))) throw new Error(`${label} escapes the project.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readJson(path, label) {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
155
|
+
} catch {
|
|
156
|
+
throw new Error(`${label} is not valid JSON.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fileCitation(projectRoot, sourceType, path, limitation = '') {
|
|
161
|
+
const root = resolve(projectRoot);
|
|
162
|
+
const absolute = resolve(root, path);
|
|
163
|
+
assertNoSymlinkEscape(root, absolute, 'Evidence citation');
|
|
164
|
+
if (!existsSync(absolute) || !lstatSync(absolute).isFile()) throw new Error(`Evidence citation does not exist: ${path}`);
|
|
165
|
+
const projectRelative = relativePath(root, absolute);
|
|
166
|
+
if (!projectRelative || projectRelative.startsWith('../') || isAbsolute(projectRelative)) throw new Error('Evidence citation must be project-relative.');
|
|
167
|
+
const content = readFileSync(absolute);
|
|
168
|
+
const recordedAt = statSync(absolute).mtime.toISOString();
|
|
169
|
+
const citationDigest = sha256(content);
|
|
170
|
+
return {
|
|
171
|
+
id: `EVD-${createHash('sha256').update(`${sourceType}:${projectRelative}:${citationDigest}`).digest('hex').slice(0, 12)}`,
|
|
172
|
+
sourceType: clean(sourceType, 'citation source type', 100),
|
|
173
|
+
path: projectRelative,
|
|
174
|
+
digest: citationDigest,
|
|
175
|
+
recordedAt,
|
|
176
|
+
limitation: String(limitation ?? '').trim().slice(0, 500),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normaliseCitation(projectRoot, value) {
|
|
181
|
+
exactFields(value, ['id', 'sourceType', 'path', 'digest', 'recordedAt', 'limitation'], 'Evidence citation');
|
|
182
|
+
const supplied = clean(value.path, 'citation path', 500);
|
|
183
|
+
if (isAbsolute(supplied) || supplied.split(/[\\/]/).includes('..')) throw new Error('Evidence citation must use a safe project-relative path.');
|
|
184
|
+
const citation = fileCitation(projectRoot, value.sourceType, supplied, value.limitation);
|
|
185
|
+
return { ...citation, recordedAt: value.recordedAt ? new Date(value.recordedAt).toISOString() : citation.recordedAt };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function entry(projectRoot, id, value, required) {
|
|
189
|
+
if (!value || typeof value !== 'object') throw new Error(`Evidence dimension ${id} must be an object.`);
|
|
190
|
+
if (!STATES.has(value.state)) throw new Error(`Evidence dimension ${id} has unsupported state: ${value.state}`);
|
|
191
|
+
const definition = DIMENSIONS.find((item) => item.id === id);
|
|
192
|
+
return {
|
|
193
|
+
id,
|
|
194
|
+
name: definition.name,
|
|
195
|
+
required,
|
|
196
|
+
state: value.state,
|
|
197
|
+
summary: clean(value.summary, `${id} summary`, 1_000),
|
|
198
|
+
citations: (value.citations ?? []).map((citation) => normaliseCitation(projectRoot, citation)),
|
|
199
|
+
limitations: [...new Set((value.limitations ?? []).map((item) => String(item).trim()).filter(Boolean))].slice(0, 20),
|
|
200
|
+
nextActions: [...new Set((value.nextActions ?? []).map((item) => String(item).trim()).filter(Boolean))].slice(0, 20),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function runGit(root, args) {
|
|
205
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: null, maxBuffer: 20 * 1024 * 1024 });
|
|
206
|
+
return result.status === 0 ? result.stdout : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function ignoredRevisionPath(path) {
|
|
210
|
+
const normalised = path.replaceAll('\\', '/');
|
|
211
|
+
return normalised.startsWith('SPECS/3.Evidence/readiness/') || normalised.startsWith('.ewai-pipeline/') || normalised.startsWith('.playwright-mcp/');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function resolveReadinessRevision(projectRoot) {
|
|
215
|
+
const root = resolve(projectRoot);
|
|
216
|
+
const head = runGit(root, ['rev-parse', 'HEAD']);
|
|
217
|
+
if (!head) return `project-files:${digest({ pipeline: existsSync(projectPaths(root).configPath) ? sha256(readFileSync(projectPaths(root).configPath)) : '' })}`;
|
|
218
|
+
const headText = head.toString('utf8').trim();
|
|
219
|
+
const diff = runGit(root, ['diff', '--binary', 'HEAD', '--', '.', ':(exclude)SPECS/3.Evidence/readiness/**']);
|
|
220
|
+
const untrackedRaw = runGit(root, ['ls-files', '--others', '--exclude-standard', '-z']);
|
|
221
|
+
const untracked = untrackedRaw ? untrackedRaw.toString('utf8').split('\0').filter(Boolean).filter((path) => !ignoredRevisionPath(path)).sort() : [];
|
|
222
|
+
const hash = createHash('sha256');
|
|
223
|
+
if (diff?.length) hash.update(diff);
|
|
224
|
+
for (const path of untracked) {
|
|
225
|
+
const absolute = resolve(root, path);
|
|
226
|
+
if (!isWithin(root, absolute) || !existsSync(absolute) || !lstatSync(absolute).isFile()) continue;
|
|
227
|
+
hash.update(`\0${path}\0`);
|
|
228
|
+
hash.update(readFileSync(absolute));
|
|
229
|
+
}
|
|
230
|
+
if (!diff?.length && !untracked.length) return headText;
|
|
231
|
+
return `${headText}:worktree-sha256:${hash.digest('hex')}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function walk(root, predicate, limit = 500) {
|
|
235
|
+
if (!existsSync(root)) return [];
|
|
236
|
+
const found = [];
|
|
237
|
+
const pending = [root];
|
|
238
|
+
while (pending.length && found.length < limit) {
|
|
239
|
+
const current = pending.shift();
|
|
240
|
+
for (const item of readdirSync(current, { withFileTypes: true })) {
|
|
241
|
+
const path = resolve(current, item.name);
|
|
242
|
+
if (item.isDirectory() && !item.isSymbolicLink()) pending.push(path);
|
|
243
|
+
else if (item.isFile() && predicate(path, item.name)) found.push(path);
|
|
244
|
+
if (found.length >= limit) break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return found;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function evidenceValue(projectRoot, state, summary, paths = [], limitation = '', extras = {}) {
|
|
251
|
+
return {
|
|
252
|
+
state,
|
|
253
|
+
summary,
|
|
254
|
+
citations: paths.filter((path) => existsSync(resolve(projectRoot, path))).map((path) => fileCitation(projectRoot, extras.sourceType ?? 'project-evidence', path, limitation)),
|
|
255
|
+
limitations: extras.limitations ?? (limitation ? [limitation] : []),
|
|
256
|
+
nextActions: extras.nextActions ?? [],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function phase(state, id) {
|
|
261
|
+
return (state.phases ?? []).find((item) => item.id === id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function firstIntentPath(projectRoot, specsRoot, slug) {
|
|
265
|
+
return walk(resolve(specsRoot, '2.Purpose/intents'), (_path, name) => name === `${slug}.md`, 20)
|
|
266
|
+
.map((path) => relativePath(projectRoot, path))[0] ?? '';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function latestTechnologyProfile(projectRoot, specsRoot) {
|
|
270
|
+
return walk(resolve(specsRoot, '3.Evidence/archaeology'), (_path, name) => name === 'technology-hosting-profile.json', 100)
|
|
271
|
+
.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs)
|
|
272
|
+
.map((path) => relativePath(projectRoot, path))[0] ?? '';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function collectProjectEvidence(projectRoot, slug, personaCatalogue) {
|
|
276
|
+
const root = resolve(projectRoot);
|
|
277
|
+
const paths = deliveryPaths(root, slug);
|
|
278
|
+
if (!existsSync(paths.statePath)) throw new Error(`Missing delivery truth for ${slug}.`);
|
|
279
|
+
const state = readJson(paths.statePath, 'Delivery state');
|
|
280
|
+
const specs = projectPaths(root).specsRoot;
|
|
281
|
+
const deliveryRelative = relativePath(root, paths.statePath);
|
|
282
|
+
const intentPath = firstIntentPath(root, specs, slug);
|
|
283
|
+
const planStandard = resolve(paths.gatesRoot, 'standards-sweep/gate-ledger.json');
|
|
284
|
+
const testGate = resolve(paths.gatesRoot, 'test-execute/gate-ledger.json');
|
|
285
|
+
const manualGate = (state.humanGates ?? []).find((item) => item.id === 'manual-qa');
|
|
286
|
+
const external = ['validate-external-plan', 'validate-external-test-plan', 'validate-external-code'].map((id) => phase(state, id)).filter(Boolean);
|
|
287
|
+
let impact = null;
|
|
288
|
+
try { impact = readImpactWorkspace(root, slug); } catch { impact = null; }
|
|
289
|
+
let scenarios = null;
|
|
290
|
+
try { scenarios = readPersonaTestScenarioWorkspace(root, slug, { personaCatalogue }); } catch { scenarios = null; }
|
|
291
|
+
let security = null;
|
|
292
|
+
try { security = assessSecurityReadiness(root); } catch (error) { security = { status: 'blocked', ready: false, reasons: [{ message: error.message }] }; }
|
|
293
|
+
const hostingPath = latestTechnologyProfile(root, specs);
|
|
294
|
+
const checklistPath = relativePath(root, resolve(paths.deliveryRoot, 'delivery-checklist.md'));
|
|
295
|
+
const qaPath = relativePath(root, resolve(paths.deliveryRoot, 'qa-walkthrough.md'));
|
|
296
|
+
const scenarioPath = relativePath(root, resolve(paths.deliveryRoot, 'test-scenarios.json'));
|
|
297
|
+
const impactPath = impact?.assessments?.[0]?.jsonPath ?? '';
|
|
298
|
+
const pipelinePath = relativePath(root, projectPaths(root).configPath);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
'purpose-acceptance': intentPath
|
|
302
|
+
? evidenceValue(root, 'satisfied', 'The accepted intent and delivery truth are present.', [intentPath, deliveryRelative], 'Intent acceptance does not prove implementation or release readiness.', { sourceType: 'intent-and-delivery' })
|
|
303
|
+
: evidenceValue(root, 'missing', 'No accepted intent document was found for this delivery.', [deliveryRelative], 'Delivery state alone does not establish intended outcomes.', { nextActions: ['Restore or reconcile the accepted intent.'] }),
|
|
304
|
+
impact: impact?.assessments?.length
|
|
305
|
+
? evidenceValue(root, impact.index?.fresh === false ? 'stale' : 'satisfied', 'A reviewed Impact assessment is available.', [impactPath], 'Impact consequences remain advisory and may require affected-user validation.', { sourceType: 'impact-assessment' })
|
|
306
|
+
: evidenceValue(root, 'not-configured', 'No reviewed Impact assessment is available.', [], 'Absence of Impact evidence does not mean there is no impact.', { nextActions: [`Prepare Impact analysis for ${slug}.`] }),
|
|
307
|
+
standards: existsSync(planStandard) && phase(state, 'standards-sweep')?.status === 'completed'
|
|
308
|
+
? evidenceValue(root, 'satisfied', 'The mandatory standards sweep completed with a recorded gate.', [relativePath(root, planStandard)], 'Standards coverage proves checked scope, not business acceptance.', { sourceType: 'standards-sweep' })
|
|
309
|
+
: evidenceValue(root, 'missing', 'The mandatory standards sweep is not complete.', [], 'Incomplete standards evidence cannot be treated as passing.', { nextActions: ['Complete the EWAI Standards Sweep.'] }),
|
|
310
|
+
tests: existsSync(testGate) && phase(state, 'test-execute')?.status === 'completed'
|
|
311
|
+
? evidenceValue(root, scenarios?.status === 'stale' ? 'stale' : 'satisfied', 'Automated test execution is complete and persona scenarios are included when reviewed.', [relativePath(root, testGate), ...(existsSync(resolve(root, scenarioPath)) ? [scenarioPath] : [])], 'Automated tests cannot replace representative-user or specialist evidence.', { sourceType: 'test-evidence' })
|
|
312
|
+
: evidenceValue(root, 'missing', 'Automated test execution is not complete.', scenarios?.status === 'missing' ? [] : [scenarioPath], 'Planned or candidate scenarios are not executed proof.', { nextActions: ['Complete Test Execute and retain results.'] }),
|
|
313
|
+
'manual-qa': manualGate?.status === 'approved' && manualGate.evidencePath
|
|
314
|
+
? evidenceValue(root, 'satisfied', 'Named Manual QA approval is recorded.', [manualGate.evidencePath], 'Manual QA applies only to its recorded scope and revision.', { sourceType: 'manual-qa' })
|
|
315
|
+
: evidenceValue(root, 'missing', 'Named Manual QA approval is pending.', [], 'Automated checks cannot approve Manual QA.', { nextActions: ['Complete the named Manual QA walkthrough.'] }),
|
|
316
|
+
'external-validation': external.length && external.every((item) => item.status === 'completed')
|
|
317
|
+
? evidenceValue(root, 'satisfied', 'Configured independent validation checkpoints completed.', external.map((item) => item.gatePath).filter(Boolean), 'External AI review is advisory and can miss issues.', { sourceType: 'external-validation' })
|
|
318
|
+
: evidenceValue(root, external.some((item) => item.status === 'not-supported') ? 'not-configured' : 'missing', 'Independent external validation is unavailable or incomplete.', [], 'The orchestrating AI is not an independent reviewer.', { nextActions: ['Arrange proportionate independent or specialist review where required.'] }),
|
|
319
|
+
security: security?.status === 'ready'
|
|
320
|
+
? evidenceValue(root, 'satisfied', 'Configured security readiness currently passes.', [pipelinePath], ASSURANCE_NOTICE, { sourceType: 'security-readiness' })
|
|
321
|
+
: security?.status === 'not-configured'
|
|
322
|
+
? evidenceValue(root, 'not-configured', 'Security validation is not configured.', [pipelinePath], ASSURANCE_NOTICE, { nextActions: ['Configure proportionate security validation or record a specialist route.'] })
|
|
323
|
+
: evidenceValue(root, 'blocking', 'Security readiness reports blocking, incomplete or stale evidence.', [pipelinePath], ASSURANCE_NOTICE, { nextActions: (security?.reasons ?? []).map((item) => item.message ?? item.code).filter(Boolean) }),
|
|
324
|
+
'technology-hosting': hostingPath
|
|
325
|
+
? evidenceValue(root, 'satisfied', 'A reviewed technology and hosting profile is available.', [hostingPath], 'Repository configuration and owner statements still require current operational confirmation.', { sourceType: 'technology-hosting-profile' })
|
|
326
|
+
: evidenceValue(root, 'not-configured', 'No reviewed technology and hosting profile is available.', [], 'Repository configuration alone does not prove current runtime hosting.', { nextActions: ['Run Archaeology technology and hosting discovery.'] }),
|
|
327
|
+
'deployment-operations': phase(state, 'delivery')?.status === 'completed' && existsSync(resolve(root, checklistPath))
|
|
328
|
+
? evidenceValue(root, 'satisfied', 'Delivery evidence records deployment and operational handoff.', [checklistPath], 'A delivery checklist is not live-service telemetry.', { sourceType: 'delivery-operations' })
|
|
329
|
+
: evidenceValue(root, 'missing', 'Delivery and operational handoff evidence is incomplete.', existsSync(resolve(root, checklistPath)) ? [checklistPath] : [], 'Implementation completion does not prove operational readiness.', { nextActions: ['Complete operational ownership, deployment and recovery evidence.'] }),
|
|
330
|
+
'documentation-training': existsSync(resolve(root, qaPath)) || existsSync(resolve(root, checklistPath))
|
|
331
|
+
? evidenceValue(root, 'conditional', 'Some delivery or QA guidance exists and needs human scope review.', [qaPath, checklistPath], 'Existence does not prove that affected users have usable current guidance.', { sourceType: 'documentation-training', nextActions: ['Confirm affected-user documentation and training needs.'] })
|
|
332
|
+
: evidenceValue(root, 'missing', 'No delivery-specific documentation or training evidence was found.', [], 'Technical tests do not prove user guidance is adequate.', { nextActions: ['Document user, operator and support changes.'] }),
|
|
333
|
+
'governance-specialist-assurance': state.approvals?.build?.decision === 'approved'
|
|
334
|
+
? evidenceValue(root, 'conditional', 'Build approval exists; additional specialist routes depend on the selected profile.', [deliveryRelative], 'Build approval is not business acceptance, specialist assurance or release permission.', { sourceType: 'governance' })
|
|
335
|
+
: evidenceValue(root, 'missing', 'No durable Build approval or specialist assurance route is recorded.', [deliveryRelative], 'Automated analysis cannot create governance authority.', { nextActions: ['Record the accountable human and specialist routes required by the profile.'] }),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function installedPersonas(projectRoot) {
|
|
340
|
+
const sourceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
341
|
+
return listPersonas([
|
|
342
|
+
resolve(sourceRoot, 'packs/personas/core/personas'),
|
|
343
|
+
personalPersonaRoot(),
|
|
344
|
+
premiumPersonaRoot(),
|
|
345
|
+
projectPersonaRoot(projectRoot),
|
|
346
|
+
]);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function personaAvailability(catalogue) {
|
|
350
|
+
return Object.fromEntries(['core', 'project', 'premium', 'personal'].map((tier) => {
|
|
351
|
+
const count = catalogue.filter((item) => item.tier === tier).length;
|
|
352
|
+
return [tier, { installed: count > 0, count, reason: count ? `${count} installed ${tier} persona${count === 1 ? '' : 's'} available.` : `No installed ${tier} personas were found.` }];
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function readinessRoot(projectRoot) {
|
|
357
|
+
return resolve(projectPaths(projectRoot).specsRoot, '3.Evidence/readiness');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function assessmentPaths(projectRoot, slug, assessmentId) {
|
|
361
|
+
if (!ASSESSMENT_PATTERN.test(assessmentId)) throw new Error(`Invalid solution readiness assessment ID: ${assessmentId}`);
|
|
362
|
+
const root = readinessRoot(projectRoot);
|
|
363
|
+
const directory = resolve(root, slug, assessmentId);
|
|
364
|
+
assertNoSymlinkEscape(root, directory, 'Readiness assessment');
|
|
365
|
+
return {
|
|
366
|
+
directory,
|
|
367
|
+
briefJson: resolve(directory, 'readiness-brief.json'),
|
|
368
|
+
briefMarkdown: resolve(directory, 'readiness-brief.md'),
|
|
369
|
+
reviewTemplate: resolve(directory, 'readiness-review.template.json'),
|
|
370
|
+
reportJson: resolve(directory, 'readiness-report.json'),
|
|
371
|
+
reportMarkdown: resolve(directory, 'readiness-report.md'),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function findAssessment(projectRoot, assessmentId) {
|
|
376
|
+
if (!ASSESSMENT_PATTERN.test(assessmentId)) throw new Error(`Invalid solution readiness assessment ID: ${assessmentId}`);
|
|
377
|
+
const matches = walk(readinessRoot(projectRoot), (path, name) => name === 'readiness-brief.json' && dirname(path).endsWith(`/${assessmentId}`), 5);
|
|
378
|
+
if (!matches.length) throw new Error(`Unknown solution readiness assessment: ${assessmentId}`);
|
|
379
|
+
if (matches.length > 1) throw new Error(`Ambiguous solution readiness assessment: ${assessmentId}`);
|
|
380
|
+
const directory = dirname(matches[0]);
|
|
381
|
+
const slug = relative(readinessRoot(projectRoot), dirname(directory)).replaceAll('\\', '/');
|
|
382
|
+
return { slug, ...assessmentPaths(projectRoot, slug, assessmentId) };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function markdownBrief(brief) {
|
|
386
|
+
const dimensions = brief.dimensions.map((item) => `| ${item.name} | ${item.required ? 'required' : 'optional'} | ${item.state} | ${item.summary.replaceAll('|', '\\|')} |`).join('\n');
|
|
387
|
+
const personas = brief.activePersonas.length ? brief.activePersonas.map((item) => `- **${item.name}** (${item.tier}) — ${item.engagementReason}`).join('\n') : '- No matching installed persona was engaged; standard model reasoning remains available.';
|
|
388
|
+
const sources = brief.dimensions.flatMap((item) => item.citations.map((citation) => `- \`${citation.id}\` ${citation.sourceType}: \`${citation.path}\` (${citation.digest.slice(0, 12)})`));
|
|
389
|
+
return `# Solution Readiness Brief — ${brief.slug}\n\n**Assessment:** \`${brief.assessmentId}\` \n**Profile:** ${brief.profile.name} (\`${brief.profile.id}\`) \n**Repository revision:** \`${brief.repositoryRevision}\` \n**Prepared:** ${brief.preparedAt}\n\n## Evidence dimensions\n\n| Dimension | Requirement | State | Summary |\n|---|---|---|---|\n${dimensions}\n\n## Active personas\n\n${personas}\n\nPersonas are advisory lenses. They do not supply evidence or approval.\n\n## Evidence citations\n\n${sources.join('\n') || '- No citations are available.'}\n\n## Notices\n\n${brief.notices.map((notice) => `> ${notice}`).join('\n\n')}\n`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function markdownReport(report) {
|
|
393
|
+
const rows = report.dimensions.map((item) => `| ${item.id} | ${item.disposition} | ${item.reason.replaceAll('|', '\\|')} |`).join('\n');
|
|
394
|
+
return `# Solution Readiness Report — ${report.slug}\n\n**Assessment:** \`${report.assessmentId}\` \n**Profile:** ${report.profile.name} (\`${report.profile.id}\`) \n**Result:** \`${report.result}\` \n**Reviewed by:** ${report.reviewedBy} \n**Reviewed:** ${report.reviewedAt}\n\n## Reviewed dimensions\n\n| Dimension | Disposition | Reason |\n|---|---|---|\n${rows}\n\n## Conditions\n\n${report.conditions.map((item) => `- ${item.text} — ${item.owner}, review by ${item.reviewBy}`).join('\n') || '- None.'}\n\n## Residual risks\n\n${report.residualRisks.map((item) => `- ${item.summary} — ${item.owner}, review by ${item.reviewBy}`).join('\n') || '- None recorded.'}\n\n## Notices\n\n${report.notices.map((notice) => `> ${notice}`).join('\n\n')}\n`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function writePreparation(paths, brief, reviewTemplate) {
|
|
398
|
+
if (existsSync(paths.directory)) {
|
|
399
|
+
const existing = readJson(paths.briefJson, 'Existing readiness brief');
|
|
400
|
+
if (existing.preparationFingerprint !== brief.preparationFingerprint) throw new Error(`Readiness assessment ${brief.assessmentId} already exists with different content.`);
|
|
401
|
+
return existing;
|
|
402
|
+
}
|
|
403
|
+
const parent = dirname(paths.directory);
|
|
404
|
+
mkdirSync(parent, { recursive: true });
|
|
405
|
+
const staging = `${paths.directory}.staging-${process.pid}`;
|
|
406
|
+
assertNoSymlinkEscape(parent, staging, 'Readiness staging directory');
|
|
407
|
+
mkdirSync(staging);
|
|
408
|
+
try {
|
|
409
|
+
writeFileSync(resolve(staging, 'readiness-brief.json'), `${JSON.stringify(brief, null, 2)}\n`);
|
|
410
|
+
writeFileSync(resolve(staging, 'readiness-brief.md'), markdownBrief(brief));
|
|
411
|
+
writeFileSync(resolve(staging, 'readiness-review.template.json'), `${JSON.stringify(reviewTemplate, null, 2)}\n`);
|
|
412
|
+
renameSync(staging, paths.directory);
|
|
413
|
+
} finally {
|
|
414
|
+
rmSync(staging, { recursive: true, force: true });
|
|
415
|
+
}
|
|
416
|
+
return brief;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function listSolutionReadinessProfiles() {
|
|
420
|
+
return profiles;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function prepareSolutionReadinessReview(projectRoot, slug, profileId, options = {}) {
|
|
424
|
+
const root = resolve(projectRoot);
|
|
425
|
+
const delivery = deliveryPaths(root, clean(slug, 'delivery slug', 100));
|
|
426
|
+
if (!existsSync(delivery.statePath)) throw new Error(`Missing delivery truth for ${slug}.`);
|
|
427
|
+
const profile = profiles.find((item) => item.id === profileId);
|
|
428
|
+
if (!profile) throw new Error(`Unknown solution readiness profile: ${profileId}`);
|
|
429
|
+
const repositoryRevision = clean(options.revision ?? resolveReadinessRevision(root), 'repository revision', 1_000);
|
|
430
|
+
const catalogue = Array.isArray(options.personaCatalogue) ? options.personaCatalogue : installedPersonas(root);
|
|
431
|
+
const rawEvidence = options.evidence ?? collectProjectEvidence(root, slug, catalogue);
|
|
432
|
+
const dimensions = DIMENSIONS.map(({ id }) => entry(root, id, rawEvidence[id] ?? { state: 'missing', summary: `No evidence projection was supplied for ${id}.`, citations: [] }, profile.requiredDimensions.includes(id)));
|
|
433
|
+
const activePersonas = selectContextualPersonas({
|
|
434
|
+
signals: [...profile.signals, ...dimensions.filter((item) => item.state !== 'satisfied').flatMap((item) => [item.id, item.summary])],
|
|
435
|
+
context: { slug, profile: profile.id, dimensions: dimensions.map((item) => ({ id: item.id, state: item.state, required: item.required })) },
|
|
436
|
+
personaCatalogue: catalogue,
|
|
437
|
+
contextLabel: `${profile.name} solution readiness`,
|
|
438
|
+
limit: 8,
|
|
439
|
+
});
|
|
440
|
+
const sourceDigest = digest(dimensions.map((item) => ({ id: item.id, state: item.state, citations: item.citations, summary: item.summary, limitations: item.limitations })));
|
|
441
|
+
const availability = personaAvailability(catalogue);
|
|
442
|
+
const preparationFingerprint = digest({ slug, profile, repositoryRevision, sourceDigest, activePersonas, availability });
|
|
443
|
+
const assessmentId = preparationFingerprint.slice(0, 16);
|
|
444
|
+
const preparedAt = options.now ? new Date(options.now).toISOString() : new Date().toISOString();
|
|
445
|
+
const base = {
|
|
446
|
+
schema: BRIEF_SCHEMA,
|
|
447
|
+
assessmentId,
|
|
448
|
+
slug,
|
|
449
|
+
profile,
|
|
450
|
+
repositoryRevision,
|
|
451
|
+
sourceDigest,
|
|
452
|
+
preparationFingerprint,
|
|
453
|
+
preparedAt,
|
|
454
|
+
dimensions,
|
|
455
|
+
activePersonas,
|
|
456
|
+
availability,
|
|
457
|
+
notices: [ASSURANCE_NOTICE, READINESS_AUTHORITY_NOTICE, POLICY_DESIGN_AUTHORITY_NOTICE],
|
|
458
|
+
guidance: { advisory: true, approvalsChanged: false, premiumSyncAttempted: false, sourceDomainsRemainAuthoritative: true },
|
|
459
|
+
};
|
|
460
|
+
const brief = { ...base, digest: digest(base) };
|
|
461
|
+
const reviewTemplate = {
|
|
462
|
+
schema: REVIEW_INPUT_SCHEMA,
|
|
463
|
+
assessmentId,
|
|
464
|
+
preparedDigest: brief.digest,
|
|
465
|
+
dimensions: dimensions.filter((item) => item.required).map((item) => ({ id: item.id, disposition: item.state === 'satisfied' ? 'accepted' : 'insufficient-evidence', reason: '', sourceRefs: item.citations.map((citation) => citation.id), conditions: [], residualRisks: [] })),
|
|
466
|
+
notes: '',
|
|
467
|
+
};
|
|
468
|
+
const paths = assessmentPaths(root, slug, assessmentId);
|
|
469
|
+
const stored = writePreparation(paths, brief, reviewTemplate);
|
|
470
|
+
const storedReviewTemplate = readJson(paths.reviewTemplate, 'Readiness review template');
|
|
471
|
+
return {
|
|
472
|
+
schema: 'ewai.solution-readiness-preparation/v1',
|
|
473
|
+
status: 'prepared',
|
|
474
|
+
assessmentId,
|
|
475
|
+
brief: stored,
|
|
476
|
+
reviewTemplate: storedReviewTemplate,
|
|
477
|
+
paths: Object.fromEntries(Object.entries(paths).filter(([key]) => key !== 'directory').map(([key, path]) => [key, relativePath(root, path)])),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function normaliseCondition(value, label, now) {
|
|
482
|
+
exactFields(value, label === 'condition' ? ['text', 'owner', 'reviewBy'] : ['summary', 'owner', 'reviewBy'], label);
|
|
483
|
+
const reviewBy = new Date(clean(value.reviewBy, `${label} reviewBy`, 100)).toISOString();
|
|
484
|
+
if (new Date(reviewBy).getTime() <= new Date(now).getTime()) throw new Error(`${label} reviewBy must be in the future.`);
|
|
485
|
+
return {
|
|
486
|
+
[label === 'condition' ? 'text' : 'summary']: clean(value[label === 'condition' ? 'text' : 'summary'], `${label} description`, 1_000),
|
|
487
|
+
owner: clean(value.owner, `${label} owner`, 200),
|
|
488
|
+
reviewBy,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function deriveResult(dimensions) {
|
|
493
|
+
if (dimensions.some((item) => item.disposition === 'blocked' || item.preparedState === 'blocking')) return 'blocked';
|
|
494
|
+
if (dimensions.some((item) => item.disposition === 'insufficient-evidence' || ['missing', 'stale', 'not-configured'].includes(item.preparedState))) return 'insufficient-evidence';
|
|
495
|
+
if (dimensions.some((item) => item.disposition === 'conditional' || item.disposition === 'not-applicable' || item.preparedState === 'conditional')) return 'conditional';
|
|
496
|
+
return 'ready-for-human-decision';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function recordSolutionReadinessReview(projectRoot, assessmentId, input, options = {}) {
|
|
500
|
+
const root = resolve(projectRoot);
|
|
501
|
+
const paths = findAssessment(root, assessmentId);
|
|
502
|
+
if (existsSync(paths.reportJson) || existsSync(paths.reportMarkdown)) throw new Error(`Solution readiness report ${assessmentId} already exists and is immutable.`);
|
|
503
|
+
const brief = readJson(paths.briefJson, 'Readiness brief');
|
|
504
|
+
exactFields(input, ['schema', 'assessmentId', 'preparedDigest', 'dimensions', 'notes'], 'Solution readiness review');
|
|
505
|
+
if (input.schema !== REVIEW_INPUT_SCHEMA) throw new Error(`Solution readiness review schema must be ${REVIEW_INPUT_SCHEMA}.`);
|
|
506
|
+
if (input.assessmentId !== assessmentId) throw new Error('Solution readiness review assessment ID does not match.');
|
|
507
|
+
if (input.preparedDigest !== brief.digest) throw new Error('Solution readiness review prepared digest does not match.');
|
|
508
|
+
const reviewedBy = clean(options.reviewedBy, 'reviewedBy', 200);
|
|
509
|
+
const reviewedAt = options.now ? new Date(options.now).toISOString() : new Date().toISOString();
|
|
510
|
+
if (!Array.isArray(input.dimensions)) throw new Error('Solution readiness review dimensions must be an array.');
|
|
511
|
+
const supplied = new Map();
|
|
512
|
+
for (const decision of input.dimensions) {
|
|
513
|
+
exactFields(decision, ['id', 'disposition', 'reason', 'sourceRefs', 'conditions', 'residualRisks'], 'Readiness dimension decision');
|
|
514
|
+
if (supplied.has(decision.id)) throw new Error(`Duplicate readiness dimension decision: ${decision.id}`);
|
|
515
|
+
supplied.set(decision.id, decision);
|
|
516
|
+
}
|
|
517
|
+
const requiredIds = new Set(brief.dimensions.filter((item) => item.required).map((item) => item.id));
|
|
518
|
+
const unexpectedIds = [...supplied.keys()].filter((id) => !requiredIds.has(id));
|
|
519
|
+
if (unexpectedIds.length) throw new Error(`Solution readiness review contains unknown or non-required dimension(s): ${unexpectedIds.join(', ')}.`);
|
|
520
|
+
const decisions = brief.dimensions.filter((item) => item.required).map((prepared) => {
|
|
521
|
+
const decision = supplied.get(prepared.id);
|
|
522
|
+
if (!decision) throw new Error(`Missing required readiness dimension decision: ${prepared.id}`);
|
|
523
|
+
if (!DISPOSITIONS.has(decision.disposition)) throw new Error(`Unsupported readiness disposition for ${prepared.id}: ${decision.disposition}`);
|
|
524
|
+
if (decision.disposition === 'accepted' && ['blocking', 'missing', 'stale', 'not-configured'].includes(prepared.state)) throw new Error(`${prepared.id} is ${prepared.state} and cannot be accepted without evidence.`);
|
|
525
|
+
const sourceRefs = [...new Set((decision.sourceRefs ?? []).map((item) => clean(item, `${prepared.id} source reference`, 100)))];
|
|
526
|
+
const dimensionRefs = new Set(prepared.citations.map((citation) => citation.id));
|
|
527
|
+
const unrelatedRefs = sourceRefs.filter((item) => !dimensionRefs.has(item));
|
|
528
|
+
if (unrelatedRefs.length) throw new Error(`${prepared.id} references unknown evidence for that dimension: ${unrelatedRefs.join(', ')}.`);
|
|
529
|
+
const conditions = (decision.conditions ?? []).map((item) => normaliseCondition(item, 'condition', reviewedAt));
|
|
530
|
+
const residualRisks = (decision.residualRisks ?? []).map((item) => normaliseCondition(item, 'residual risk', reviewedAt));
|
|
531
|
+
if (decision.disposition === 'conditional' && !conditions.length) throw new Error(`${prepared.id} conditional disposition requires an owned condition.`);
|
|
532
|
+
return {
|
|
533
|
+
id: prepared.id,
|
|
534
|
+
preparedState: prepared.state,
|
|
535
|
+
disposition: decision.disposition,
|
|
536
|
+
reason: clean(decision.reason, `${prepared.id} reason`, 2_000),
|
|
537
|
+
sourceRefs,
|
|
538
|
+
conditions,
|
|
539
|
+
residualRisks,
|
|
540
|
+
};
|
|
541
|
+
});
|
|
542
|
+
const base = {
|
|
543
|
+
schema: REPORT_SCHEMA,
|
|
544
|
+
assessmentId,
|
|
545
|
+
slug: brief.slug,
|
|
546
|
+
profile: brief.profile,
|
|
547
|
+
repositoryRevision: brief.repositoryRevision,
|
|
548
|
+
preparedDigest: brief.digest,
|
|
549
|
+
reviewedBy,
|
|
550
|
+
reviewedAt,
|
|
551
|
+
result: deriveResult(decisions),
|
|
552
|
+
dimensions: decisions,
|
|
553
|
+
conditions: decisions.flatMap((item) => item.conditions.map((condition) => ({ dimensionId: item.id, ...condition }))),
|
|
554
|
+
residualRisks: decisions.flatMap((item) => item.residualRisks.map((risk) => ({ dimensionId: item.id, ...risk }))),
|
|
555
|
+
activePersonas: brief.activePersonas,
|
|
556
|
+
notes: String(input.notes ?? '').trim().slice(0, 5_000),
|
|
557
|
+
notices: [ASSURANCE_NOTICE, READINESS_AUTHORITY_NOTICE, POLICY_DESIGN_AUTHORITY_NOTICE],
|
|
558
|
+
authority: { advisory: true, manualQaChanged: false, securityDispositionChanged: false, policyDecisionChanged: false, releaseChanged: false },
|
|
559
|
+
};
|
|
560
|
+
const report = { ...base, digest: digest(base) };
|
|
561
|
+
const markdown = markdownReport(report);
|
|
562
|
+
const stagingJson = `${paths.reportJson}.staging-${process.pid}`;
|
|
563
|
+
const stagingMarkdown = `${paths.reportMarkdown}.staging-${process.pid}`;
|
|
564
|
+
try {
|
|
565
|
+
writeFileSync(stagingJson, `${JSON.stringify(report, null, 2)}\n`);
|
|
566
|
+
writeFileSync(stagingMarkdown, markdown);
|
|
567
|
+
if (existsSync(paths.reportJson) || existsSync(paths.reportMarkdown)) throw new Error(`Solution readiness report ${assessmentId} already exists and is immutable.`);
|
|
568
|
+
renameSync(stagingJson, paths.reportJson);
|
|
569
|
+
renameSync(stagingMarkdown, paths.reportMarkdown);
|
|
570
|
+
} finally {
|
|
571
|
+
rmSync(stagingJson, { force: true });
|
|
572
|
+
rmSync(stagingMarkdown, { force: true });
|
|
573
|
+
}
|
|
574
|
+
return { schema: 'ewai.solution-readiness-recording/v1', status: 'recorded', assessmentId, report, paths: { reportJson: relativePath(root, paths.reportJson), reportMarkdown: relativePath(root, paths.reportMarkdown) } };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function readSolutionReadinessStatus(projectRoot, assessmentId, options = {}) {
|
|
578
|
+
const root = resolve(projectRoot);
|
|
579
|
+
const paths = findAssessment(root, assessmentId);
|
|
580
|
+
const brief = readJson(paths.briefJson, 'Readiness brief');
|
|
581
|
+
const report = existsSync(paths.reportJson) ? readJson(paths.reportJson, 'Readiness report') : null;
|
|
582
|
+
const currentRevision = clean(options.revision ?? resolveReadinessRevision(root), 'repository revision', 1_000);
|
|
583
|
+
const drift = [];
|
|
584
|
+
const { digest: briefDigest, ...briefBody } = brief;
|
|
585
|
+
if (digest(briefBody) !== briefDigest) drift.push({ kind: 'brief-digest', status: 'changed' });
|
|
586
|
+
if (report) {
|
|
587
|
+
const { digest: reportDigest, ...reportBody } = report;
|
|
588
|
+
if (digest(reportBody) !== reportDigest) drift.push({ kind: 'report-digest', status: 'changed' });
|
|
589
|
+
if (report.preparedDigest !== brief.digest) drift.push({ kind: 'report-binding', status: 'changed' });
|
|
590
|
+
}
|
|
591
|
+
if (currentRevision !== brief.repositoryRevision) drift.push({ kind: 'repository-revision', prepared: brief.repositoryRevision, current: currentRevision });
|
|
592
|
+
const currentProfile = profiles.find((item) => item.id === brief.profile?.id);
|
|
593
|
+
if (!currentProfile || digest(currentProfile) !== digest(brief.profile)) drift.push({ kind: 'profile', prepared: brief.profile?.id ?? '', current: currentProfile?.id ?? 'missing' });
|
|
594
|
+
for (const citation of brief.dimensions.flatMap((item) => item.citations)) {
|
|
595
|
+
const absolute = resolve(root, citation.path);
|
|
596
|
+
try {
|
|
597
|
+
assertNoSymlinkEscape(root, absolute, 'Evidence citation');
|
|
598
|
+
if (!existsSync(absolute)) drift.push({ kind: 'cited-evidence', citationId: citation.id, path: citation.path, status: 'missing' });
|
|
599
|
+
else if (sha256(readFileSync(absolute)) !== citation.digest) drift.push({ kind: 'cited-evidence', citationId: citation.id, path: citation.path, status: 'changed' });
|
|
600
|
+
} catch {
|
|
601
|
+
drift.push({ kind: 'cited-evidence', citationId: citation.id, path: citation.path, status: 'unsafe' });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
schema: 'ewai.solution-readiness-status/v1',
|
|
606
|
+
assessmentId,
|
|
607
|
+
slug: brief.slug,
|
|
608
|
+
status: drift.length ? 'stale' : report ? 'current' : 'awaiting-human-review',
|
|
609
|
+
preparedAt: brief.preparedAt,
|
|
610
|
+
reviewedAt: report?.reviewedAt ?? null,
|
|
611
|
+
result: report?.result ?? null,
|
|
612
|
+
profile: brief.profile,
|
|
613
|
+
repositoryRevision: brief.repositoryRevision,
|
|
614
|
+
currentRevision,
|
|
615
|
+
drift,
|
|
616
|
+
activePersonas: brief.activePersonas,
|
|
617
|
+
notices: [ASSURANCE_NOTICE, READINESS_AUTHORITY_NOTICE, POLICY_DESIGN_AUTHORITY_NOTICE],
|
|
618
|
+
paths: { briefJson: relativePath(root, paths.briefJson), reportJson: existsSync(paths.reportJson) ? relativePath(root, paths.reportJson) : null },
|
|
619
|
+
};
|
|
620
|
+
}
|