@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
package/src/context.mjs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
13
|
+
import YAML from 'yaml';
|
|
14
|
+
import { projectPaths } from './paths.mjs';
|
|
15
|
+
|
|
16
|
+
const classifications = new Set(['public', 'internal', 'confidential', 'restricted']);
|
|
17
|
+
const cloudPolicies = new Set(['allowed', 'denied', 'unknown']);
|
|
18
|
+
const ignoredDirectories = new Set([
|
|
19
|
+
'.git', '.ewai-pipeline', '.idea', '.vscode', 'node_modules', 'vendor', 'dist', 'build'
|
|
20
|
+
]);
|
|
21
|
+
const supportedExtensions = new Set([
|
|
22
|
+
'.csv', '.doc', '.docx', '.eml', '.htm', '.html', '.ics', '.json', '.log', '.markdown',
|
|
23
|
+
'.mbox', '.md', '.msg', '.odt', '.ods', '.pdf', '.ppt', '.pptx', '.rtf', '.text', '.tsv',
|
|
24
|
+
'.txt', '.xls', '.xlsx', '.xml', '.yaml', '.yml',
|
|
25
|
+
'.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp',
|
|
26
|
+
'.m4a', '.mp3', '.mp4', '.wav', '.webm'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function normalized(path) {
|
|
30
|
+
return path.split(sep).join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function slugify(value) {
|
|
34
|
+
return String(value ?? '')
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
37
|
+
.replace(/^-+|-+$/g, '')
|
|
38
|
+
.slice(0, 60) || 'project-context';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dateStamp(value) {
|
|
42
|
+
return new Date(value).toISOString().slice(0, 10);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function inside(parent, child) {
|
|
46
|
+
const path = relative(parent, child);
|
|
47
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scanSource(root, exclusions, maxFiles) {
|
|
51
|
+
const files = [];
|
|
52
|
+
const skipped = { symlinks: 0, unsupported: 0, excluded: 0 };
|
|
53
|
+
|
|
54
|
+
function walk(directory) {
|
|
55
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
56
|
+
const path = resolve(directory, entry.name);
|
|
57
|
+
const sourceRelative = normalized(relative(root, path));
|
|
58
|
+
if (exclusions.some((excluded) => sourceRelative === excluded || sourceRelative.startsWith(`${excluded}/`))) {
|
|
59
|
+
skipped.excluded += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (entry.isSymbolicLink()) {
|
|
63
|
+
skipped.symlinks += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
if (!ignoredDirectories.has(entry.name)) walk(path);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!entry.isFile()) continue;
|
|
71
|
+
const extension = extname(entry.name).toLowerCase();
|
|
72
|
+
if (!supportedExtensions.has(extension)) {
|
|
73
|
+
skipped.unsupported += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (files.length >= maxFiles) throw new Error(`Context folder exceeds the ${maxFiles} file registration limit; narrow the folder or add exclusions`);
|
|
77
|
+
const stats = statSync(path);
|
|
78
|
+
files.push({
|
|
79
|
+
relativePath: sourceRelative,
|
|
80
|
+
extension,
|
|
81
|
+
size: stats.size,
|
|
82
|
+
modifiedAt: stats.mtime.toISOString()
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
walk(root);
|
|
88
|
+
return { files, skipped };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function inventorySummary(files) {
|
|
92
|
+
const extensions = {};
|
|
93
|
+
let totalBytes = 0;
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
totalBytes += file.size;
|
|
96
|
+
extensions[file.extension] = (extensions[file.extension] ?? 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
return { fileCount: files.length, totalBytes, extensions };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readRegistry(path) {
|
|
102
|
+
if (!existsSync(path)) return { schema: 'ewai.context-source-registry/v1', sources: [] };
|
|
103
|
+
const registry = YAML.parse(readFileSync(path, 'utf8')) ?? {};
|
|
104
|
+
registry.schema ??= 'ewai.context-source-registry/v1';
|
|
105
|
+
registry.sources ??= [];
|
|
106
|
+
return registry;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function registerContextSource(projectRoot, sourcePath, options = {}) {
|
|
110
|
+
if (!options.confirmed) throw new Error('Context registration requires explicit user confirmation');
|
|
111
|
+
const paths = projectPaths(projectRoot);
|
|
112
|
+
if (!existsSync(paths.configPath)) throw new Error('Initialize EWAI before registering project context');
|
|
113
|
+
|
|
114
|
+
const candidate = resolve(sourcePath);
|
|
115
|
+
if (!existsSync(candidate) || !lstatSync(candidate).isDirectory()) {
|
|
116
|
+
throw new Error(`Context source is not a readable folder: ${candidate}`);
|
|
117
|
+
}
|
|
118
|
+
const sourceRoot = realpathSync(candidate);
|
|
119
|
+
const project = realpathSync(paths.projectRoot);
|
|
120
|
+
if (sourceRoot === project) throw new Error('Choose a dedicated context folder rather than the entire project');
|
|
121
|
+
if (inside(sourceRoot, project)) throw new Error('Choose a context folder that does not contain the project itself');
|
|
122
|
+
|
|
123
|
+
const classification = options.classification ?? 'confidential';
|
|
124
|
+
if (!classifications.has(classification)) throw new Error(`Unsupported context classification: ${classification}`);
|
|
125
|
+
const cloudProcessing = options.cloudProcessing ?? 'unknown';
|
|
126
|
+
if (!cloudPolicies.has(cloudProcessing)) throw new Error(`Unsupported cloud-processing policy: ${cloudProcessing}`);
|
|
127
|
+
|
|
128
|
+
const label = String(options.label || basename(sourceRoot)).trim();
|
|
129
|
+
const slug = slugify(label);
|
|
130
|
+
const exclusions = (options.exclusions ?? [])
|
|
131
|
+
.map((item) => normalized(String(item).replace(/^\.\//, '').replace(/\/$/, '')))
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
const registeredAt = new Date(options.now ?? Date.now()).toISOString();
|
|
134
|
+
const scanned = scanSource(sourceRoot, exclusions, options.maxFiles ?? 10_000);
|
|
135
|
+
const inventory = inventorySummary(scanned.files);
|
|
136
|
+
const sourceHash = createHash('sha256').update(`${project}\0${sourceRoot}`).digest('hex').slice(0, 12);
|
|
137
|
+
const sourceId = `context.${slug}.${sourceHash}`;
|
|
138
|
+
const inventoryFingerprint = createHash('sha256').update(JSON.stringify(scanned.files)).digest('hex');
|
|
139
|
+
|
|
140
|
+
const privateRoot = resolve(paths.runtimeRoot, 'context/sources');
|
|
141
|
+
const privateManifest = resolve(privateRoot, `${sourceId}.json`);
|
|
142
|
+
if (existsSync(privateManifest) && !options.force) {
|
|
143
|
+
throw new Error(`Context source is already registered: ${sourceId}`);
|
|
144
|
+
}
|
|
145
|
+
mkdirSync(privateRoot, { recursive: true });
|
|
146
|
+
writeFileSync(privateManifest, `${JSON.stringify({
|
|
147
|
+
schema: 'ewai.context-source-private/v1',
|
|
148
|
+
sourceId,
|
|
149
|
+
label,
|
|
150
|
+
sourceRoot,
|
|
151
|
+
classification,
|
|
152
|
+
cloudProcessing,
|
|
153
|
+
exclusions,
|
|
154
|
+
registeredAt,
|
|
155
|
+
inventoryFingerprint,
|
|
156
|
+
files: scanned.files,
|
|
157
|
+
skipped: scanned.skipped
|
|
158
|
+
}, null, 2)}\n`, 'utf8');
|
|
159
|
+
|
|
160
|
+
const registryPath = resolve(paths.specsRoot, '1.Scope/research/context-sources.yaml');
|
|
161
|
+
const registry = readRegistry(registryPath);
|
|
162
|
+
const publicEntry = {
|
|
163
|
+
id: sourceId,
|
|
164
|
+
label,
|
|
165
|
+
classification,
|
|
166
|
+
cloud_processing: cloudProcessing,
|
|
167
|
+
status: 'registered',
|
|
168
|
+
registered_at: registeredAt,
|
|
169
|
+
inventory: {
|
|
170
|
+
file_count: inventory.fileCount,
|
|
171
|
+
total_bytes: inventory.totalBytes,
|
|
172
|
+
extensions: inventory.extensions
|
|
173
|
+
},
|
|
174
|
+
inventory_fingerprint: inventoryFingerprint,
|
|
175
|
+
review_status: 'pending'
|
|
176
|
+
};
|
|
177
|
+
registry.sources = registry.sources.filter((source) => source.id !== sourceId);
|
|
178
|
+
registry.sources.push(publicEntry);
|
|
179
|
+
mkdirSync(resolve(paths.specsRoot, '1.Scope/research'), { recursive: true });
|
|
180
|
+
writeFileSync(registryPath, YAML.stringify(registry, { lineWidth: 0 }), 'utf8');
|
|
181
|
+
|
|
182
|
+
const bundleRoot = resolve(
|
|
183
|
+
paths.specsRoot,
|
|
184
|
+
'3.Evidence/context-imports',
|
|
185
|
+
`${dateStamp(registeredAt)}-${slug}-${sourceHash}`
|
|
186
|
+
);
|
|
187
|
+
mkdirSync(bundleRoot, { recursive: true });
|
|
188
|
+
const registrationPath = resolve(bundleRoot, 'source-registration.yaml');
|
|
189
|
+
writeFileSync(registrationPath, YAML.stringify({
|
|
190
|
+
schema: 'ewai.context-source-registration/v1',
|
|
191
|
+
...publicEntry,
|
|
192
|
+
private_manifest: `.ewai-pipeline/context/sources/${sourceId}.json`,
|
|
193
|
+
analysis_status: 'pending'
|
|
194
|
+
}, { lineWidth: 0 }), 'utf8');
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
schema: 'ewai.context-register-result/v1',
|
|
198
|
+
projectRoot: paths.projectRoot,
|
|
199
|
+
sourceId,
|
|
200
|
+
label,
|
|
201
|
+
classification,
|
|
202
|
+
cloudProcessing,
|
|
203
|
+
inventory,
|
|
204
|
+
skipped: scanned.skipped,
|
|
205
|
+
privateManifest,
|
|
206
|
+
registryPath,
|
|
207
|
+
bundleRoot,
|
|
208
|
+
registrationPath
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {constants,openSync,closeSync,fstatSync,lstatSync,readFileSync,writeFileSync,renameSync,unlinkSync,realpathSync,fsyncSync} from 'node:fs';
|
|
2
|
+
import {dirname,relative,resolve,isAbsolute} from 'node:path';
|
|
3
|
+
import {createHash,randomUUID} from 'node:crypto';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import {projectPaths} from './paths.mjs';
|
|
6
|
+
|
|
7
|
+
export const DASHBOARD_VIEWS=Object.freeze([
|
|
8
|
+
['portfolio','Portfolio','Compare work and decisions across several projects.','Across projects and teams'],
|
|
9
|
+
['team-hub','Team Hub','Share approved resources and project summaries with your team.','Across projects and teams'],
|
|
10
|
+
['rollout','Governed Rollout','Coordinate adoption across organisations or multiple projects.','Across projects and teams'],
|
|
11
|
+
['starters','Starters','Inspect and deliberately apply approved starting material.','Project tools'],
|
|
12
|
+
['policies','Policy Gates','Inspect organisation policy requirements and their evidence.','Project tools'],
|
|
13
|
+
['security','Security Validation','Review configured security checks, findings and decisions.','Project tools'],
|
|
14
|
+
['hooks','Hooks','Inspect explicitly configured lifecycle integrations and failures.','Project tools'],
|
|
15
|
+
['context-inspector','AI context diagnostics','See which evidence and personas EWAI selects for an AI task.','Deeper analysis'],
|
|
16
|
+
['phase-studio','Contributions','Add attributed business or technical evidence to an active delivery.','Deeper analysis'],
|
|
17
|
+
].map(([id,title,description,group])=>Object.freeze({id,title,description,group})));
|
|
18
|
+
|
|
19
|
+
function fail(message,statusCode=400){const error=new Error(message);error.statusCode=statusCode;error.dashboardPreferenceError=true;throw error;}
|
|
20
|
+
function record(value){return value!==null&&typeof value==='object'&&!Array.isArray(value);}
|
|
21
|
+
function keys(value,allowed){if(!record(value)||Object.keys(value).some(key=>!allowed.includes(key)))fail('Dashboard preferences contain unsupported fields.');}
|
|
22
|
+
export function normaliseDashboardPreferences(value={}){
|
|
23
|
+
keys(value,['optional_views','sidebar_collapsed']);
|
|
24
|
+
const optional=value.optional_views===undefined?{}:value.optional_views;keys(optional,DASHBOARD_VIEWS.map(v=>v.id));
|
|
25
|
+
if(value.sidebar_collapsed!==undefined&&typeof value.sidebar_collapsed!=='boolean')fail('Sidebar preferences must be true or false.');
|
|
26
|
+
for(const flag of Object.values(optional))if(typeof flag!=='boolean')fail('Dashboard view preferences must be true or false.');
|
|
27
|
+
return {sidebar_collapsed:value.sidebar_collapsed??false,optional_views:Object.fromEntries(DASHBOARD_VIEWS.map(v=>[v.id,optional[v.id]??false]))};
|
|
28
|
+
}
|
|
29
|
+
export function premiumPersonasActive(premium){return premium?.access==='available'&&premium?.installed===true&&premium?.verified===true;}
|
|
30
|
+
const digest=bytes=>'sha256:'+createHash('sha256').update(bytes).digest('hex');
|
|
31
|
+
function safePath(paths){
|
|
32
|
+
const root=realpathSync(paths.projectRoot),parent=dirname(paths.configPath),rel=relative(root,realpathSync(parent));
|
|
33
|
+
if(rel.startsWith('..')||isAbsolute(rel))fail('Dashboard preferences require a safe project configuration path.',403);
|
|
34
|
+
let cursor=parent;
|
|
35
|
+
while(cursor!==paths.projectRoot){if(lstatSync(cursor).isSymbolicLink())fail('Dashboard preferences cannot use a symbolic configuration directory.',403);const next=dirname(cursor);if(next===cursor)fail('Dashboard preferences require a safe project configuration path.',403);cursor=next;}
|
|
36
|
+
const stat=lstatSync(paths.configPath);
|
|
37
|
+
if(!stat.isFile()||stat.isSymbolicLink()||stat.nlink!==1)fail('Dashboard preferences require a regular, non-symbolic configuration file.',403);
|
|
38
|
+
if(stat.size>2*1024*1024)fail('The project configuration is too large for dashboard preference editing.',413);
|
|
39
|
+
return stat;
|
|
40
|
+
}
|
|
41
|
+
function snapshot(paths){
|
|
42
|
+
const stat=safePath(paths);let fd;
|
|
43
|
+
try{
|
|
44
|
+
fd=openSync(paths.configPath,constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
45
|
+
const opened=fstatSync(fd);if(opened.ino!==stat.ino||opened.dev!==stat.dev)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
46
|
+
const bytes=readFileSync(fd);if(bytes.length>2*1024*1024)fail('The project configuration is too large for dashboard preference editing.',413);
|
|
47
|
+
const document=YAML.parseDocument(bytes.toString('utf8'));
|
|
48
|
+
if(document.errors.length)fail('The project configuration contains invalid YAML. Fix it before editing dashboard preferences.',422);
|
|
49
|
+
let config;try{config=document.toJS({maxAliasCount:100});}catch{fail('The project configuration cannot be safely read.',422);}
|
|
50
|
+
if(config?.schema!=='ewai.project/v1'||config?.specs?.root!==paths.specsRelative)fail('The project configuration and SPECS locator must agree before editing dashboard preferences.',422);
|
|
51
|
+
return {document,preferences:normaliseDashboardPreferences(config.dashboard),digest:digest(bytes),mode:stat.mode&0o777};
|
|
52
|
+
}finally{if(fd!==undefined)closeSync(fd);}
|
|
53
|
+
}
|
|
54
|
+
function guarded(operation){try{return operation();}catch(error){if(error.dashboardPreferenceError)throw error;fail('Dashboard preferences could not be read or saved. Check the project configuration and file permissions.',409);}}
|
|
55
|
+
function projection(state){return {schema:'ewai.dashboard-preferences/v1',digest:state.digest,preferences:state.preferences,views:DASHBOARD_VIEWS};}
|
|
56
|
+
export function readDashboardPreferences(projectRoot){return guarded(()=>projection(snapshot(projectPaths(projectRoot))));}
|
|
57
|
+
export function saveDashboardPreferences(projectRoot,input){
|
|
58
|
+
return guarded(()=>{
|
|
59
|
+
keys(input,['confirmed','expectedDigest','preferences']);
|
|
60
|
+
if(input.confirmed!==true)fail('Confirm the dashboard preference changes before saving.');
|
|
61
|
+
if(!/^sha256:[a-f0-9]{64}$/.test(String(input.expectedDigest??'')))fail('Reload dashboard preferences before saving.');
|
|
62
|
+
if(!record(input.preferences))fail('Provide the dashboard preferences to save.');
|
|
63
|
+
const preferences=normaliseDashboardPreferences(input.preferences),paths=projectPaths(projectRoot);
|
|
64
|
+
safePath(paths);const lock=paths.configPath+'.dashboard-preferences.lock';let lockFd,tempFd,temporary;
|
|
65
|
+
try{
|
|
66
|
+
try{lockFd=openSync(lock,'wx',0o600);}catch{fail('Dashboard preferences are being edited. Retry after that save finishes.',409);}
|
|
67
|
+
const before=snapshot(paths);if(before.digest!==input.expectedDigest)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
68
|
+
before.document.set('dashboard',preferences);const content=before.document.toString();
|
|
69
|
+
temporary=paths.configPath+'.dashboard-'+randomUUID()+'.tmp';tempFd=openSync(temporary,'wx',before.mode);
|
|
70
|
+
writeFileSync(tempFd,content,'utf8');fsyncSync(tempFd);closeSync(tempFd);tempFd=undefined;
|
|
71
|
+
if(snapshot(paths).digest!==before.digest)fail('Project configuration changed. Reload the settings and try again.',409);
|
|
72
|
+
safePath(paths);renameSync(temporary,paths.configPath);temporary=undefined;
|
|
73
|
+
return projection(snapshot(paths));
|
|
74
|
+
}finally{
|
|
75
|
+
if(tempFd!==undefined)closeSync(tempFd);
|
|
76
|
+
if(temporary)unlinkSync(temporary);
|
|
77
|
+
if(lockFd!==undefined){closeSync(lockFd);unlinkSync(lock);}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { validateDesignSystemReceipt } from './design-system-application.mjs';
|
|
6
|
+
import { policyGateRequirementsForPhase } from './policy-gate-integration.mjs';
|
|
7
|
+
|
|
8
|
+
const contractPath = fileURLToPath(new URL('../config/delivery-artifacts.yaml', import.meta.url));
|
|
9
|
+
const contract = YAML.parse(readFileSync(contractPath, 'utf8'));
|
|
10
|
+
|
|
11
|
+
function taskCompletionPaths(deliveryRoot) {
|
|
12
|
+
const graphPath = resolve(deliveryRoot, 'task-graph.json');
|
|
13
|
+
if (!existsSync(graphPath)) return ['task-graph.json'];
|
|
14
|
+
let graph;
|
|
15
|
+
try {
|
|
16
|
+
graph = JSON.parse(readFileSync(graphPath, 'utf8'));
|
|
17
|
+
} catch {
|
|
18
|
+
return ['task-graph.json (valid JSON required)'];
|
|
19
|
+
}
|
|
20
|
+
const tasks = Array.isArray(graph.tasks) ? graph.tasks : [];
|
|
21
|
+
if (!tasks.length) return ['task-graph.json (at least one task required)'];
|
|
22
|
+
return tasks.flatMap((task, index) => {
|
|
23
|
+
const configured = String(task.report_path ?? '').trim();
|
|
24
|
+
const id = String(task.id ?? '').trim();
|
|
25
|
+
const report = configured || (id ? `tasks/${id}/report.md` : `task-graph.json (task ${index + 1} requires id or report_path)`);
|
|
26
|
+
const evidence = String(task.evidence_path ?? '').trim()
|
|
27
|
+
|| (id ? `tasks/${id}/evidence.json` : `task-graph.json (task ${index + 1} requires id or evidence_path)`);
|
|
28
|
+
return [report, evidence];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function projectRootForDelivery(deliveryRoot) {
|
|
33
|
+
let candidate = resolve(deliveryRoot);
|
|
34
|
+
while (dirname(candidate) !== candidate) {
|
|
35
|
+
if (existsSync(resolve(candidate, '.ewai-pipeline/project.json'))) return candidate;
|
|
36
|
+
candidate = dirname(candidate);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function requiredPhaseArtefacts(deliveryRoot, phaseId, providers = [], cycles = []) {
|
|
42
|
+
const definition = contract.phases?.[phaseId];
|
|
43
|
+
if (!definition) return [];
|
|
44
|
+
const required = [...(definition.required ?? [])];
|
|
45
|
+
const projectRoot = projectRootForDelivery(deliveryRoot);
|
|
46
|
+
if (projectRoot && definition.organisation_policy) {
|
|
47
|
+
const slug = basename(resolve(deliveryRoot));
|
|
48
|
+
if (policyGateRequirementsForPhase(projectRoot, slug, phaseId).requirements.length) {
|
|
49
|
+
required.push(...(definition.organisation_policy.required ?? []));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (definition.validation_cycles) {
|
|
53
|
+
required.push(`${phaseId}/validation-cycles.json`);
|
|
54
|
+
for (const cycle of cycles) {
|
|
55
|
+
if (cycle.responsePath) required.push(cycle.responsePath);
|
|
56
|
+
if (cycle.fixPath) required.push(cycle.fixPath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (definition.task_reports) required.push(...taskCompletionPaths(deliveryRoot));
|
|
60
|
+
return [...new Set(required)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function validatePhaseArtefacts(deliveryRoot, phaseId, providers = [], cycles = []) {
|
|
64
|
+
const required = requiredPhaseArtefacts(deliveryRoot, phaseId, providers, cycles);
|
|
65
|
+
const root = resolve(deliveryRoot);
|
|
66
|
+
const missing = required.filter((path) => {
|
|
67
|
+
if (!path || path.includes('(')) return true;
|
|
68
|
+
const absolute = resolve(root, path);
|
|
69
|
+
const rel = relative(root, absolute);
|
|
70
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel)) return true;
|
|
71
|
+
return !existsSync(absolute) || !statSync(absolute).isFile();
|
|
72
|
+
});
|
|
73
|
+
if (missing.length) {
|
|
74
|
+
const lines = missing.map((path) => `- ${path}`).join('\n');
|
|
75
|
+
throw new Error(`Phase ${phaseId} is missing canonical delivery artefacts:\n${lines}`);
|
|
76
|
+
}
|
|
77
|
+
if (phaseId === 'ui-design') validatePrototypeManifest(root);
|
|
78
|
+
return required;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function validatePrototypeManifest(deliveryRoot) {
|
|
82
|
+
const root = resolve(deliveryRoot);
|
|
83
|
+
const manifestPath = resolve(root, 'ui-design-assets/prototypes/manifest.json');
|
|
84
|
+
let manifest;
|
|
85
|
+
try {
|
|
86
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new Error(`UI Design prototype manifest is not valid JSON: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
const errors = [];
|
|
91
|
+
const statePath = resolve(root, 'delivery-state.json');
|
|
92
|
+
let requiredContract = '';
|
|
93
|
+
if (existsSync(statePath)) {
|
|
94
|
+
try {
|
|
95
|
+
requiredContract = JSON.parse(readFileSync(statePath, 'utf8'))?.artefactContracts?.prototype ?? '';
|
|
96
|
+
} catch {
|
|
97
|
+
errors.push('delivery-state.json must be valid JSON before validating prototype evidence');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!['ewai.prototype-manifest/v1', 'ewai.prototype-manifest/v2', 'ewai.prototype-manifest/v3'].includes(manifest.schema)) {
|
|
101
|
+
errors.push('schema must be ewai.prototype-manifest/v1, ewai.prototype-manifest/v2 or ewai.prototype-manifest/v3');
|
|
102
|
+
}
|
|
103
|
+
if (['ewai.prototype-manifest/v2', 'ewai.prototype-manifest/v3'].includes(requiredContract) && manifest.schema !== requiredContract) {
|
|
104
|
+
errors.push(`this delivery requires ${requiredContract}; migrate the prototype manifest and link its governed design evidence`);
|
|
105
|
+
}
|
|
106
|
+
if (!['selected', 'approved'].includes(String(manifest.status ?? '').trim().toLowerCase())) {
|
|
107
|
+
errors.push('status must be selected or approved');
|
|
108
|
+
}
|
|
109
|
+
const selected = manifest.selected ?? {};
|
|
110
|
+
for (const key of ['title', 'path', 'decision']) {
|
|
111
|
+
if (!String(selected[key] ?? '').trim()) errors.push(`selected.${key} is required`);
|
|
112
|
+
}
|
|
113
|
+
const prototypePath = resolve(root, String(selected.path ?? ''));
|
|
114
|
+
const prototypeRoot = resolve(root, 'ui-design-assets/prototypes');
|
|
115
|
+
const prototypeRelative = relative(prototypeRoot, prototypePath);
|
|
116
|
+
if (!selected.path || isAbsolute(String(selected.path)) || !prototypeRelative || prototypeRelative.startsWith('..') || isAbsolute(prototypeRelative)) {
|
|
117
|
+
errors.push('selected.path must identify a file inside ui-design-assets/prototypes');
|
|
118
|
+
} else if (!existsSync(prototypePath) || !statSync(prototypePath).isFile()) {
|
|
119
|
+
errors.push(`selected prototype does not exist: ${selected.path}`);
|
|
120
|
+
} else if (lstatSync(prototypeRoot).isSymbolicLink() || lstatSync(prototypePath).isSymbolicLink()) {
|
|
121
|
+
errors.push('selected prototype and prototypes directory may not be a symbolic link');
|
|
122
|
+
} else if (relative(realpathSync(prototypeRoot), realpathSync(prototypePath)).startsWith('..')) {
|
|
123
|
+
errors.push('selected prototype may not escape the prototypes directory through a symbolic link');
|
|
124
|
+
} else if (!['.html', '.htm'].includes(extname(prototypePath).toLowerCase())) {
|
|
125
|
+
errors.push('selected prototype must be an HTML entry point');
|
|
126
|
+
}
|
|
127
|
+
const registration = manifest.registration ?? {};
|
|
128
|
+
if (registration.kind !== 'prototype') errors.push('registration.kind must be prototype');
|
|
129
|
+
if (registration.status !== 'active') errors.push('registration.status must be active');
|
|
130
|
+
if (registration.path !== selected.path) errors.push('registration.path must match selected.path');
|
|
131
|
+
if (['ewai.prototype-manifest/v2', 'ewai.prototype-manifest/v3'].includes(manifest.schema)) {
|
|
132
|
+
const application = manifest.designSystem ?? {};
|
|
133
|
+
let receipt = null;
|
|
134
|
+
try {
|
|
135
|
+
receipt = validateDesignSystemReceipt(root, application.receiptPath);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
errors.push(error.message);
|
|
138
|
+
}
|
|
139
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(String(application.receiptDigest ?? ''))) errors.push('designSystem.receiptDigest must be SHA-256');
|
|
140
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(String(application.effectiveDigest ?? ''))) errors.push('designSystem.effectiveDigest must be SHA-256');
|
|
141
|
+
if (receipt) {
|
|
142
|
+
if (receipt.delivery !== basename(root)) errors.push('design-system receipt delivery must match this delivery');
|
|
143
|
+
if (application.receiptDigest !== receipt.digest) errors.push('designSystem.receiptDigest must match the receipt');
|
|
144
|
+
if (application.effectiveDigest !== receipt.designSystem.effectiveDigest) errors.push('designSystem.effectiveDigest must match the receipt');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (manifest.schema === 'ewai.prototype-manifest/v3') {
|
|
148
|
+
const reviews = manifest.reviews ?? {};
|
|
149
|
+
const reviewRoot = resolve(root, 'ui-design-assets/prototype-iterations');
|
|
150
|
+
const reviewRecords = {};
|
|
151
|
+
for (const [key, schema, expectedFolder] of [
|
|
152
|
+
['plan', 'ewai.prototype-plan-review/v1', 'plans'],
|
|
153
|
+
['finalCycle', 'ewai.prototype-cycle-review/v1', 'cycles'],
|
|
154
|
+
]) {
|
|
155
|
+
const link = reviews[key] ?? {};
|
|
156
|
+
if (!String(link.path ?? '').trim()) {
|
|
157
|
+
errors.push(`reviews.${key}.path is required`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(String(link.digest ?? ''))) errors.push(`reviews.${key}.digest must be SHA-256`);
|
|
161
|
+
const candidate = resolve(root, String(link.path));
|
|
162
|
+
const expectedRoot = resolve(reviewRoot, expectedFolder);
|
|
163
|
+
const rel = relative(expectedRoot, candidate);
|
|
164
|
+
if (isAbsolute(String(link.path)) || !rel || rel.startsWith('..') || isAbsolute(rel)) {
|
|
165
|
+
errors.push(`reviews.${key}.path must identify immutable evidence inside ui-design-assets/prototype-iterations/${expectedFolder}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!existsSync(candidate) || !statSync(candidate).isFile()) {
|
|
169
|
+
errors.push(`reviews.${key}.path does not exist: ${link.path}`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (lstatSync(expectedRoot).isSymbolicLink() || lstatSync(candidate).isSymbolicLink()) {
|
|
173
|
+
errors.push(`reviews.${key}.path and its evidence directory may not be symbolic links`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const record = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
178
|
+
reviewRecords[key] = record;
|
|
179
|
+
if (record.schema !== schema) errors.push(`reviews.${key}.path must contain ${schema}`);
|
|
180
|
+
if (record.contentDigest !== link.digest) errors.push(`reviews.${key}.digest must match the immutable review record`);
|
|
181
|
+
if (record.deliverySlug && record.deliverySlug !== basename(root)) errors.push(`reviews.${key} delivery must match this delivery`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
errors.push(`reviews.${key}.path must contain valid review JSON: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const plan = reviewRecords.plan;
|
|
187
|
+
const cycle = reviewRecords.finalCycle;
|
|
188
|
+
if (cycle) {
|
|
189
|
+
if (!Number.isInteger(reviews.finalCycle?.cycleNumber) || reviews.finalCycle.cycleNumber < 1 || reviews.finalCycle.cycleNumber > 3) {
|
|
190
|
+
errors.push('reviews.finalCycle.cycleNumber must be an integer from 1 to 3');
|
|
191
|
+
} else if (cycle.cycleNumber !== reviews.finalCycle.cycleNumber) {
|
|
192
|
+
errors.push('reviews.finalCycle.cycleNumber must match the immutable cycle review');
|
|
193
|
+
}
|
|
194
|
+
if (plan && cycle.planReviewDigest !== plan.contentDigest) errors.push('the final cycle must reference the linked reviewed plan');
|
|
195
|
+
if (cycle.prototype?.entryPath !== selected.path) errors.push('the final reviewed cycle must reference selected.path');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (errors.length) throw new Error(`UI Design prototype manifest is incomplete:\n- ${errors.join('\n- ')}`);
|
|
199
|
+
return manifest;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function deliveryArtefactContract() {
|
|
203
|
+
return structuredClone(contract);
|
|
204
|
+
}
|