@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,509 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
statSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
10
|
+
import YAML from 'yaml';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { projectPaths } from './paths.mjs';
|
|
13
|
+
import { loadProjectConfig } from './project.mjs';
|
|
14
|
+
import { ASSURANCE_NOTICE } from './security-validation-config.mjs';
|
|
15
|
+
import { selectContextualPersonas } from './runtime/persona-engagement.mjs';
|
|
16
|
+
|
|
17
|
+
export const PORTFOLIO_SCHEMA = 'ewai.portfolio/v1';
|
|
18
|
+
export const PORTFOLIO_WORKSPACE_SCHEMA = 'ewai.portfolio-workspace/v1';
|
|
19
|
+
export const PORTFOLIO_ASSURANCE_NOTICE = ASSURANCE_NOTICE;
|
|
20
|
+
export const PORTFOLIO_ADVISORY_NOTICE = 'Portfolio and persona analysis is advisory. Child project approvals, accepted risk, Manual QA and release decisions remain with named accountable humans.';
|
|
21
|
+
|
|
22
|
+
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
23
|
+
const MAX_CHILD_FILE_BYTES = 512 * 1024;
|
|
24
|
+
const MAX_CHILD_FILES = 200;
|
|
25
|
+
const DEFAULT_STALE_AFTER_DAYS = 30;
|
|
26
|
+
const slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(100);
|
|
27
|
+
const boundedText = (maximum) => z.string().trim().min(1).max(maximum);
|
|
28
|
+
|
|
29
|
+
const memberSchema = z.object({
|
|
30
|
+
id: slug,
|
|
31
|
+
kind: z.enum(['portfolio', 'programme', 'project']),
|
|
32
|
+
name: boundedText(160),
|
|
33
|
+
owner: boundedText(160),
|
|
34
|
+
parent: slug.optional(),
|
|
35
|
+
repository: boundedText(120).optional(),
|
|
36
|
+
project_path: boundedText(500).optional(),
|
|
37
|
+
}).strict().superRefine((member, context) => {
|
|
38
|
+
if (member.kind === 'project') {
|
|
39
|
+
if (!member.repository) context.addIssue({ code: 'custom', path: ['repository'], message: 'project member requires repository' });
|
|
40
|
+
if (!member.project_path) context.addIssue({ code: 'custom', path: ['project_path'], message: 'project member requires project_path' });
|
|
41
|
+
} else if (member.repository || member.project_path) {
|
|
42
|
+
context.addIssue({ code: 'custom', message: 'only project members may declare repository and project_path' });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const dependencySchema = z.object({
|
|
47
|
+
id: slug,
|
|
48
|
+
from: slug,
|
|
49
|
+
to: slug,
|
|
50
|
+
rationale: boundedText(500),
|
|
51
|
+
owner: boundedText(160),
|
|
52
|
+
}).strict();
|
|
53
|
+
|
|
54
|
+
const manifestSchema = z.object({
|
|
55
|
+
schema: z.literal(PORTFOLIO_SCHEMA),
|
|
56
|
+
id: slug,
|
|
57
|
+
name: boundedText(160),
|
|
58
|
+
owner: boundedText(160),
|
|
59
|
+
members: z.array(memberSchema).min(1).max(100),
|
|
60
|
+
dependencies: z.array(dependencySchema).max(400).default([]),
|
|
61
|
+
}).strict();
|
|
62
|
+
|
|
63
|
+
function diagnostic(code, message, path = '') {
|
|
64
|
+
return { code, message, ...(path ? { path } : {}) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function safeRelative(root, path) {
|
|
68
|
+
return relative(resolve(root), resolve(path)).replaceAll('\\', '/') || '.';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function within(root, candidate) {
|
|
72
|
+
const path = relative(resolve(root), resolve(candidate));
|
|
73
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normaliseRelative(value) {
|
|
77
|
+
const text = String(value ?? '').trim().replaceAll('\\', '/');
|
|
78
|
+
if (!text || text.includes('\0') || text.startsWith('/') || /^[A-Za-z]:\//.test(text)) return null;
|
|
79
|
+
const segments = text.split('/');
|
|
80
|
+
if (segments.includes('..')) return null;
|
|
81
|
+
const normalised = segments.filter((segment) => segment && segment !== '.').join('/');
|
|
82
|
+
return normalised || '.';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertNoSymbolicTraversal(root, relativePath) {
|
|
86
|
+
if (lstatSync(root).isSymbolicLink()) return false;
|
|
87
|
+
if (relativePath === '.') return true;
|
|
88
|
+
let cursor = root;
|
|
89
|
+
for (const segment of relativePath.split('/')) {
|
|
90
|
+
cursor = resolve(cursor, segment);
|
|
91
|
+
if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function graphCycle(nodes, edges) {
|
|
97
|
+
const outgoing = new Map(nodes.map((node) => [node, []]));
|
|
98
|
+
for (const [from, to] of edges) outgoing.get(from)?.push(to);
|
|
99
|
+
const visiting = new Set();
|
|
100
|
+
const visited = new Set();
|
|
101
|
+
function visit(node) {
|
|
102
|
+
if (visiting.has(node)) return true;
|
|
103
|
+
if (visited.has(node)) return false;
|
|
104
|
+
visiting.add(node);
|
|
105
|
+
for (const target of outgoing.get(node) ?? []) if (visit(target)) return true;
|
|
106
|
+
visiting.delete(node);
|
|
107
|
+
visited.add(node);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return nodes.some(visit);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hierarchyDepth(memberId, byId, trail = new Set()) {
|
|
114
|
+
if (trail.has(memberId)) return Number.POSITIVE_INFINITY;
|
|
115
|
+
const member = byId.get(memberId);
|
|
116
|
+
if (!member?.parent) return 1;
|
|
117
|
+
return 1 + hierarchyDepth(member.parent, byId, new Set([...trail, memberId]));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function validatePortfolioManifest(input) {
|
|
121
|
+
const parsed = manifestSchema.safeParse(input);
|
|
122
|
+
if (!parsed.success) {
|
|
123
|
+
return {
|
|
124
|
+
valid: false,
|
|
125
|
+
diagnostics: parsed.error.issues.map((issue) => diagnostic(
|
|
126
|
+
'portfolio.schema',
|
|
127
|
+
issue.message,
|
|
128
|
+
issue.path.join('.'),
|
|
129
|
+
)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const manifest = parsed.data;
|
|
133
|
+
const diagnostics = [];
|
|
134
|
+
const byId = new Map();
|
|
135
|
+
for (const member of manifest.members) {
|
|
136
|
+
if (byId.has(member.id)) diagnostics.push(diagnostic('portfolio.duplicate-member', `Duplicate member ID: ${member.id}`, 'members'));
|
|
137
|
+
else byId.set(member.id, member);
|
|
138
|
+
}
|
|
139
|
+
const portfolioRoots = manifest.members.filter((member) => member.kind === 'portfolio' && !member.parent);
|
|
140
|
+
if (portfolioRoots.length !== 1) diagnostics.push(diagnostic('portfolio.root-count', 'Exactly one root portfolio member is required.', 'members'));
|
|
141
|
+
for (const member of manifest.members) {
|
|
142
|
+
if (member.kind === 'portfolio' && member.parent) diagnostics.push(diagnostic('portfolio.portfolio-parent', `Portfolio member ${member.id} may not have a parent.`, `members.${member.id}.parent`));
|
|
143
|
+
if (member.kind !== 'portfolio' && !member.parent) diagnostics.push(diagnostic('portfolio.missing-parent', `Member ${member.id} requires a parent.`, `members.${member.id}.parent`));
|
|
144
|
+
if (!member.parent) continue;
|
|
145
|
+
const parent = byId.get(member.parent);
|
|
146
|
+
if (!parent) diagnostics.push(diagnostic('portfolio.missing-parent', `Member ${member.id} references a missing parent.`, `members.${member.id}.parent`));
|
|
147
|
+
else if (parent.kind === 'project') diagnostics.push(diagnostic('portfolio.project-parent', `Member ${member.id} may not be nested below a project.`, `members.${member.id}.parent`));
|
|
148
|
+
}
|
|
149
|
+
const hierarchyEdges = manifest.members.filter((member) => member.parent && byId.has(member.parent)).map((member) => [member.parent, member.id]);
|
|
150
|
+
if (graphCycle([...byId.keys()], hierarchyEdges)) diagnostics.push(diagnostic('portfolio.hierarchy-cycle', 'Portfolio hierarchy contains a cycle.', 'members'));
|
|
151
|
+
for (const member of manifest.members) {
|
|
152
|
+
if (hierarchyDepth(member.id, byId) > 8) {
|
|
153
|
+
diagnostics.push(diagnostic('portfolio.hierarchy-depth', `Member ${member.id} exceeds hierarchy depth 8.`, `members.${member.id}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const dependencyIds = new Set();
|
|
157
|
+
const dependencyEdges = [];
|
|
158
|
+
for (const dependency of manifest.dependencies) {
|
|
159
|
+
if (dependencyIds.has(dependency.id)) diagnostics.push(diagnostic('portfolio.duplicate-dependency', `Duplicate dependency ID: ${dependency.id}`, 'dependencies'));
|
|
160
|
+
dependencyIds.add(dependency.id);
|
|
161
|
+
const from = byId.get(dependency.from);
|
|
162
|
+
const to = byId.get(dependency.to);
|
|
163
|
+
if (!from || !to) diagnostics.push(diagnostic('portfolio.missing-dependency-target', `Dependency ${dependency.id} references a missing member.`, `dependencies.${dependency.id}`));
|
|
164
|
+
else if (from.kind !== 'project' || to.kind !== 'project') diagnostics.push(diagnostic('portfolio.dependency-kind', `Dependency ${dependency.id} may connect project members only.`, `dependencies.${dependency.id}`));
|
|
165
|
+
if (dependency.from === dependency.to) diagnostics.push(diagnostic('portfolio.self-dependency', `Dependency ${dependency.id} may not target itself.`, `dependencies.${dependency.id}`));
|
|
166
|
+
if (from && to && from.kind === 'project' && to.kind === 'project' && dependency.from !== dependency.to) dependencyEdges.push([dependency.from, dependency.to]);
|
|
167
|
+
}
|
|
168
|
+
const projectIds = manifest.members.filter((member) => member.kind === 'project').map((member) => member.id);
|
|
169
|
+
if (graphCycle(projectIds, dependencyEdges)) diagnostics.push(diagnostic('portfolio.dependency-cycle', 'Portfolio dependencies contain a cycle.', 'dependencies'));
|
|
170
|
+
return { valid: diagnostics.length === 0, diagnostics, manifest };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function configuredRepositories(projectRoot, config) {
|
|
174
|
+
const repositories = new Map();
|
|
175
|
+
const diagnostics = [];
|
|
176
|
+
for (const repository of config.repositories ?? []) {
|
|
177
|
+
const name = String(repository?.name ?? '').trim();
|
|
178
|
+
const path = normaliseRelative(repository?.path);
|
|
179
|
+
if (!name || !path || repositories.has(name)) {
|
|
180
|
+
diagnostics.push(diagnostic('portfolio.invalid-repository', 'Configured repository identity or path is invalid.'));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const absolute = resolve(projectRoot, path);
|
|
184
|
+
if (!within(projectRoot, absolute) || !existsSync(absolute) || !lstatSync(absolute).isDirectory() || lstatSync(absolute).isSymbolicLink()) {
|
|
185
|
+
diagnostics.push(diagnostic('portfolio.invalid-repository', `Configured repository ${name} is unavailable or unsafe.`));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const realRoot = realpathSync(absolute);
|
|
189
|
+
if (!within(realpathSync(projectRoot), realRoot)) {
|
|
190
|
+
diagnostics.push(diagnostic('portfolio.invalid-repository', `Configured repository ${name} escapes the workspace.`));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
repositories.set(name, { name, path, root: realRoot });
|
|
194
|
+
}
|
|
195
|
+
return { repositories, diagnostics };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resolvePortfolioMembers(projectRoot, input) {
|
|
199
|
+
const validation = validatePortfolioManifest(input);
|
|
200
|
+
if (!validation.valid) return { valid: false, members: [], diagnostics: validation.diagnostics };
|
|
201
|
+
let loaded;
|
|
202
|
+
try {
|
|
203
|
+
loaded = loadProjectConfig(projectRoot);
|
|
204
|
+
} catch {
|
|
205
|
+
return { valid: false, members: [], diagnostics: [diagnostic('portfolio.project-config', 'The portfolio host project configuration is unavailable or invalid.')] };
|
|
206
|
+
}
|
|
207
|
+
const configured = configuredRepositories(resolve(projectRoot), loaded.config);
|
|
208
|
+
const diagnostics = [...configured.diagnostics];
|
|
209
|
+
const resolvedRoots = new Map();
|
|
210
|
+
const members = validation.manifest.members.map((member) => {
|
|
211
|
+
const base = { id: member.id, kind: member.kind, name: member.name, owner: member.owner, parent: member.parent ?? null };
|
|
212
|
+
if (member.kind !== 'project') return base;
|
|
213
|
+
const repository = configured.repositories.get(member.repository);
|
|
214
|
+
if (!repository) {
|
|
215
|
+
diagnostics.push(diagnostic('portfolio.unknown-repository', `Project ${member.id} references an unknown configured repository.`, `members.${member.id}.repository`));
|
|
216
|
+
return { ...base, repository: member.repository, projectPath: member.project_path };
|
|
217
|
+
}
|
|
218
|
+
const projectPath = normaliseRelative(member.project_path);
|
|
219
|
+
if (!projectPath) {
|
|
220
|
+
diagnostics.push(diagnostic('portfolio.unsafe-path', `Project ${member.id} has an unsafe project_path.`, `members.${member.id}.project_path`));
|
|
221
|
+
return { ...base, repository: member.repository, projectPath: member.project_path };
|
|
222
|
+
}
|
|
223
|
+
if (!assertNoSymbolicTraversal(repository.root, projectPath)) {
|
|
224
|
+
diagnostics.push(diagnostic('portfolio.symbolic-path', `Project ${member.id} traverses a symbolic path.`, `members.${member.id}.project_path`));
|
|
225
|
+
return { ...base, repository: member.repository, projectPath };
|
|
226
|
+
}
|
|
227
|
+
const absolute = resolve(repository.root, projectPath);
|
|
228
|
+
if (!within(repository.root, absolute)) {
|
|
229
|
+
diagnostics.push(diagnostic('portfolio.unsafe-path', `Project ${member.id} escapes its configured repository.`, `members.${member.id}.project_path`));
|
|
230
|
+
return { ...base, repository: member.repository, projectPath };
|
|
231
|
+
}
|
|
232
|
+
if (!existsSync(absolute) || !lstatSync(absolute).isDirectory() || lstatSync(absolute).isSymbolicLink()) {
|
|
233
|
+
diagnostics.push(diagnostic('portfolio.missing-project', `Project ${member.id} root is unavailable.`, `members.${member.id}.project_path`));
|
|
234
|
+
return { ...base, repository: member.repository, projectPath };
|
|
235
|
+
}
|
|
236
|
+
const realProjectRoot = realpathSync(absolute);
|
|
237
|
+
if (!within(repository.root, realProjectRoot)) {
|
|
238
|
+
diagnostics.push(diagnostic('portfolio.unsafe-path', `Project ${member.id} escapes its configured repository.`, `members.${member.id}.project_path`));
|
|
239
|
+
return { ...base, repository: member.repository, projectPath };
|
|
240
|
+
}
|
|
241
|
+
if (resolvedRoots.has(realProjectRoot)) {
|
|
242
|
+
diagnostics.push(diagnostic('portfolio.duplicate-root', `Projects ${resolvedRoots.get(realProjectRoot)} and ${member.id} resolve to the same root.`, `members.${member.id}.project_path`));
|
|
243
|
+
} else resolvedRoots.set(realProjectRoot, member.id);
|
|
244
|
+
return { ...base, repository: member.repository, projectPath, root: realProjectRoot };
|
|
245
|
+
});
|
|
246
|
+
return { valid: diagnostics.length === 0, members, diagnostics, manifest: validation.manifest };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function readPortfolioResolution(projectRoot, options = {}) {
|
|
250
|
+
const root = resolve(projectRoot);
|
|
251
|
+
let paths;
|
|
252
|
+
try {
|
|
253
|
+
paths = projectPaths(root);
|
|
254
|
+
} catch {
|
|
255
|
+
return { status: 'invalid', diagnostics: [diagnostic('portfolio.project-root', 'The EWAI project root is invalid.')] };
|
|
256
|
+
}
|
|
257
|
+
const manifestPath = resolve(paths.specsRoot, '1.Scope/portfolio.yaml');
|
|
258
|
+
if (!existsSync(manifestPath)) return { status: 'not-configured', diagnostics: [] };
|
|
259
|
+
if (lstatSync(manifestPath).isSymbolicLink() || statSync(manifestPath).size > MAX_MANIFEST_BYTES) {
|
|
260
|
+
return { status: 'invalid', diagnostics: [diagnostic('portfolio.manifest-file', 'Portfolio manifest is symbolic or exceeds its size limit.')] };
|
|
261
|
+
}
|
|
262
|
+
let raw;
|
|
263
|
+
try {
|
|
264
|
+
raw = YAML.parse(readFileSync(manifestPath, 'utf8'));
|
|
265
|
+
} catch {
|
|
266
|
+
return { status: 'invalid', diagnostics: [diagnostic('portfolio.manifest-yaml', 'Portfolio manifest is invalid YAML.')] };
|
|
267
|
+
}
|
|
268
|
+
const resolution = resolvePortfolioMembers(root, raw);
|
|
269
|
+
const unavailableCodes = new Set(['portfolio.missing-project']);
|
|
270
|
+
const blocking = options.allowUnavailableProjects
|
|
271
|
+
? resolution.diagnostics.filter((item) => !unavailableCodes.has(item.code))
|
|
272
|
+
: resolution.diagnostics;
|
|
273
|
+
return {
|
|
274
|
+
status: blocking.length ? 'invalid' : 'ready',
|
|
275
|
+
diagnostics: resolution.diagnostics,
|
|
276
|
+
resolution,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function boundedRead(path, label) {
|
|
281
|
+
const size = statSync(path).size;
|
|
282
|
+
if (size > MAX_CHILD_FILE_BYTES) throw new Error(`${label} exceeds its size limit`);
|
|
283
|
+
return readFileSync(path, 'utf8');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseFrontmatter(content) {
|
|
287
|
+
const match = String(content).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
288
|
+
if (!match) return {};
|
|
289
|
+
return YAML.parse(match[1]) ?? {};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function safeFiles(root, predicate, depth = 0, found = []) {
|
|
293
|
+
if (!existsSync(root) || depth > 8 || found.length >= MAX_CHILD_FILES) return found;
|
|
294
|
+
for (const entry of readdirSync(root, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
295
|
+
if (entry.isSymbolicLink()) continue;
|
|
296
|
+
const path = resolve(root, entry.name);
|
|
297
|
+
if (entry.isDirectory()) safeFiles(path, predicate, depth + 1, found);
|
|
298
|
+
else if (entry.isFile() && predicate(path, entry.name)) found.push(path);
|
|
299
|
+
if (found.length >= MAX_CHILD_FILES) break;
|
|
300
|
+
}
|
|
301
|
+
return found;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function childIntentEvidence(childRoot, specsRoot) {
|
|
305
|
+
const intentsRoot = resolve(specsRoot, '2.Purpose/intents');
|
|
306
|
+
const markdownFiles = safeFiles(intentsRoot, (_path, name) => name.endsWith('.md'));
|
|
307
|
+
const records = [];
|
|
308
|
+
let disagreement = false;
|
|
309
|
+
for (const path of markdownFiles) {
|
|
310
|
+
try {
|
|
311
|
+
const metadata = parseFrontmatter(boundedRead(path, 'Intent metadata'));
|
|
312
|
+
const slugValue = String(metadata.slug ?? '').trim();
|
|
313
|
+
if (!slugValue) continue;
|
|
314
|
+
const jsonPath = path.replace(/\.md$/i, '.json');
|
|
315
|
+
let structured = null;
|
|
316
|
+
if (existsSync(jsonPath) && !lstatSync(jsonPath).isSymbolicLink()) structured = JSON.parse(boundedRead(jsonPath, 'Intent structured metadata'));
|
|
317
|
+
const markdownStatus = String(metadata.status ?? metadata.delivery_status ?? '').trim() || 'unknown';
|
|
318
|
+
const structuredStatus = String(structured?.status ?? '').trim() || null;
|
|
319
|
+
if (structuredStatus && structuredStatus !== markdownStatus) disagreement = true;
|
|
320
|
+
records.push({
|
|
321
|
+
slug: slugValue.slice(0, 100),
|
|
322
|
+
status: markdownStatus.slice(0, 80),
|
|
323
|
+
structuredStatus: structuredStatus?.slice(0, 80) ?? null,
|
|
324
|
+
reference: safeRelative(childRoot, path),
|
|
325
|
+
});
|
|
326
|
+
} catch {
|
|
327
|
+
records.push({ slug: 'unreadable', status: 'invalid', structuredStatus: null, reference: safeRelative(childRoot, path) });
|
|
328
|
+
disagreement = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { count: records.length, disagreement, items: records.slice(0, 50) };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function childDeliveryEvidence(childRoot, specsRoot) {
|
|
335
|
+
const buildRoot = resolve(specsRoot, '6.Build');
|
|
336
|
+
const paths = existsSync(buildRoot) ? readdirSync(buildRoot, { withFileTypes: true })
|
|
337
|
+
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
|
|
338
|
+
.map((entry) => resolve(buildRoot, entry.name, 'delivery-state.json'))
|
|
339
|
+
.filter((path) => existsSync(path) && !lstatSync(path).isSymbolicLink())
|
|
340
|
+
.slice(0, MAX_CHILD_FILES) : [];
|
|
341
|
+
const states = paths.flatMap((path) => {
|
|
342
|
+
try {
|
|
343
|
+
const state = JSON.parse(boundedRead(path, 'Delivery state'));
|
|
344
|
+
if (state.schema !== 'ewai.delivery-state/v1') return [];
|
|
345
|
+
const manualQa = (state.humanGates ?? []).find((gate) => gate.id === 'manual-qa');
|
|
346
|
+
return [{
|
|
347
|
+
slug: String(state.slug ?? '').slice(0, 100),
|
|
348
|
+
status: String(state.status ?? 'unknown').slice(0, 80),
|
|
349
|
+
currentPhase: String(state.currentPhase ?? 'unknown').slice(0, 80),
|
|
350
|
+
blockedReason: String(state.blockedReason ?? '').slice(0, 240),
|
|
351
|
+
updatedAt: String(state.updatedAt ?? '').slice(0, 80) || null,
|
|
352
|
+
buildApproved: Boolean(state.approvals?.build),
|
|
353
|
+
manualQa: String(manualQa?.status ?? 'missing').slice(0, 80),
|
|
354
|
+
reference: safeRelative(childRoot, path),
|
|
355
|
+
}];
|
|
356
|
+
} catch {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
}).sort((left, right) => String(right.updatedAt ?? '').localeCompare(String(left.updatedAt ?? '')));
|
|
360
|
+
const current = states.find((state) => !['completed', 'cancelled'].includes(state.status)) ?? states[0] ?? null;
|
|
361
|
+
return {
|
|
362
|
+
count: states.length,
|
|
363
|
+
status: current?.status ?? 'missing',
|
|
364
|
+
currentPhase: current?.currentPhase ?? 'missing',
|
|
365
|
+
blockedReason: current?.blockedReason ?? '',
|
|
366
|
+
updatedAt: current?.updatedAt ?? null,
|
|
367
|
+
buildApproved: current?.buildApproved ?? false,
|
|
368
|
+
manualQa: current?.manualQa ?? 'missing',
|
|
369
|
+
reference: current?.reference ?? null,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function freshness(updatedAt, nowValue, staleAfterDays) {
|
|
374
|
+
if (!updatedAt) return { status: 'unknown', updatedAt: null };
|
|
375
|
+
const updated = Date.parse(updatedAt);
|
|
376
|
+
const now = Date.parse(nowValue);
|
|
377
|
+
if (!Number.isFinite(updated) || !Number.isFinite(now)) return { status: 'unknown', updatedAt };
|
|
378
|
+
const ageDays = Math.max(0, Math.floor((now - updated) / 86_400_000));
|
|
379
|
+
return { status: ageDays > staleAfterDays ? 'stale' : 'current', updatedAt, ageDays };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function readChildEvidence(member, options) {
|
|
383
|
+
try {
|
|
384
|
+
const paths = projectPaths(member.root);
|
|
385
|
+
if (!existsSync(paths.configPath)) return { status: 'unavailable', reason: 'missing-project-config' };
|
|
386
|
+
const loaded = loadProjectConfig(member.root);
|
|
387
|
+
const intents = childIntentEvidence(member.root, loaded.paths.specsRoot);
|
|
388
|
+
const delivery = childDeliveryEvidence(member.root, loaded.paths.specsRoot);
|
|
389
|
+
const evidenceFreshness = freshness(delivery.updatedAt, options.now, options.staleAfterDays);
|
|
390
|
+
let status = 'ready';
|
|
391
|
+
if (!delivery.count) status = 'missing';
|
|
392
|
+
if (evidenceFreshness.status === 'stale') status = 'stale';
|
|
393
|
+
if (intents.disagreement) status = 'disagreement';
|
|
394
|
+
return {
|
|
395
|
+
status,
|
|
396
|
+
project: { schema: loaded.config.schema, name: String(loaded.config.project?.name ?? member.name).slice(0, 160) },
|
|
397
|
+
intents,
|
|
398
|
+
delivery,
|
|
399
|
+
freshness: evidenceFreshness,
|
|
400
|
+
};
|
|
401
|
+
} catch {
|
|
402
|
+
return { status: 'unavailable', reason: 'invalid-project-evidence' };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function hierarchyMembers(members) {
|
|
407
|
+
const children = new Map();
|
|
408
|
+
for (const member of members) {
|
|
409
|
+
const parent = member.parent ?? '__root__';
|
|
410
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
411
|
+
children.get(parent).push(member);
|
|
412
|
+
}
|
|
413
|
+
for (const values of children.values()) values.sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
|
|
414
|
+
const ordered = [];
|
|
415
|
+
function append(parent, depth) {
|
|
416
|
+
for (const member of children.get(parent) ?? []) {
|
|
417
|
+
const { root: _root, ...safe } = member;
|
|
418
|
+
ordered.push({ ...safe, depth });
|
|
419
|
+
append(member.id, depth + 1);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
append('__root__', 0);
|
|
423
|
+
return ordered;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function personaAvailability(catalogue) {
|
|
427
|
+
const counts = Object.fromEntries(['project', 'premium', 'personal', 'core'].map((tier) => [tier, catalogue.filter((persona) => String(persona.tier || 'core') === tier).length]));
|
|
428
|
+
return {
|
|
429
|
+
baseline: { available: true, method: 'standard-llm-with-installed-project-and-core-personas' },
|
|
430
|
+
project: { installed: counts.project > 0, count: counts.project },
|
|
431
|
+
core: { installed: counts.core > 0, count: counts.core },
|
|
432
|
+
personal: { installed: counts.personal > 0, count: counts.personal, required: false },
|
|
433
|
+
premium: { installed: counts.premium > 0, count: counts.premium, required: false, syncAttempted: false },
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function baseWorkspace(catalogue) {
|
|
438
|
+
return {
|
|
439
|
+
schema: PORTFOLIO_WORKSPACE_SCHEMA,
|
|
440
|
+
personaAvailability: personaAvailability(catalogue),
|
|
441
|
+
review: {
|
|
442
|
+
standardLlmAvailable: true,
|
|
443
|
+
questions: [
|
|
444
|
+
'Which declared dependency is most exposed to missing or stale evidence?',
|
|
445
|
+
'Which child owner needs to make the next decision?',
|
|
446
|
+
'Where does observed evidence support or challenge a declared relationship?',
|
|
447
|
+
'Which conclusion is a persona hypothesis rather than a project fact?',
|
|
448
|
+
],
|
|
449
|
+
evidenceClasses: ['declared', 'observed', 'inferred', 'persona-hypothesis', 'human-decision'],
|
|
450
|
+
},
|
|
451
|
+
notices: { advisory: PORTFOLIO_ADVISORY_NOTICE, security: PORTFOLIO_ASSURANCE_NOTICE },
|
|
452
|
+
guide: 'Docs/project-portfolio-orchestration-guide.md',
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function attentionFor(member) {
|
|
457
|
+
const evidence = member.evidence;
|
|
458
|
+
if (!evidence) return [];
|
|
459
|
+
const items = [];
|
|
460
|
+
if (evidence.status === 'unavailable') items.push({ code: 'portfolio.unavailable-member', member: member.id, owner: member.owner, reason: evidence.reason, evidenceClass: 'observed' });
|
|
461
|
+
if (evidence.status === 'missing') items.push({ code: 'portfolio.missing-delivery-state', member: member.id, owner: member.owner, reason: 'No readable child delivery state exists.', evidenceClass: 'observed' });
|
|
462
|
+
if (evidence.freshness?.status === 'stale') items.push({ code: 'portfolio.stale-member', member: member.id, owner: member.owner, reason: 'Child delivery evidence is older than the configured freshness window.', evidenceClass: 'observed' });
|
|
463
|
+
if (evidence.intents?.disagreement) items.push({ code: 'portfolio.intent-disagreement', member: member.id, owner: member.owner, reason: 'Markdown and structured intent status disagree.', evidenceClass: 'observed' });
|
|
464
|
+
if (evidence.delivery?.blockedReason) items.push({ code: 'portfolio.child-blocked', member: member.id, owner: member.owner, reason: evidence.delivery.blockedReason, evidenceClass: 'declared-child-state' });
|
|
465
|
+
return items;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function readPortfolioWorkspace(projectRoot, options = {}) {
|
|
469
|
+
const root = resolve(projectRoot);
|
|
470
|
+
const catalogue = Array.isArray(options.personas) ? options.personas : Array.isArray(options.personaCatalogue) ? options.personaCatalogue : [];
|
|
471
|
+
const base = baseWorkspace(catalogue);
|
|
472
|
+
const portfolioResolution = readPortfolioResolution(root);
|
|
473
|
+
if (portfolioResolution.status !== 'ready') return { ...base, status: portfolioResolution.status, diagnostics: portfolioResolution.diagnostics, members: [], dependencies: [], observedDependencies: [], attention: [], activePersonas: [] };
|
|
474
|
+
const { resolution } = portfolioResolution;
|
|
475
|
+
const now = options.now ?? new Date().toISOString();
|
|
476
|
+
const staleAfterDays = Math.max(1, Math.min(3650, Number(options.staleAfterDays ?? DEFAULT_STALE_AFTER_DAYS)));
|
|
477
|
+
const members = hierarchyMembers(resolution.members).map((member) => member.kind === 'project'
|
|
478
|
+
? { ...member, evidence: readChildEvidence(resolution.members.find((candidate) => candidate.id === member.id), { now, staleAfterDays }) }
|
|
479
|
+
: { ...member, evidence: { status: 'declared', evidenceClass: 'declared' } });
|
|
480
|
+
const dependencies = resolution.manifest.dependencies.map((dependency) => ({ ...dependency, evidenceClass: 'declared' }));
|
|
481
|
+
const attention = members.flatMap(attentionFor);
|
|
482
|
+
const signals = [
|
|
483
|
+
'portfolio', 'programme', 'project', 'outcome', 'ownership', 'delivery', 'dependency', 'evidence', 'risk',
|
|
484
|
+
resolution.manifest.name, resolution.manifest.owner,
|
|
485
|
+
...members.flatMap((member) => [member.name, member.owner, member.evidence?.status]),
|
|
486
|
+
...dependencies.flatMap((dependency) => [dependency.rationale, dependency.owner]),
|
|
487
|
+
...attention.flatMap((item) => [item.code, item.reason]),
|
|
488
|
+
String(options.focus ?? '').slice(0, 500),
|
|
489
|
+
];
|
|
490
|
+
const activePersonas = selectContextualPersonas({
|
|
491
|
+
signals,
|
|
492
|
+
context: { portfolio: resolution.manifest.id, memberStates: members.map((member) => [member.id, member.evidence?.status]), dependencyCount: dependencies.length, focus: String(options.focus ?? '').slice(0, 500) },
|
|
493
|
+
contextLabel: options.focus ? `portfolio context: ${String(options.focus).slice(0, 120)}` : 'portfolio hierarchy, evidence and dependencies',
|
|
494
|
+
personaCatalogue: catalogue,
|
|
495
|
+
limit: 6,
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
...base,
|
|
499
|
+
status: attention.length ? 'attention' : 'ready',
|
|
500
|
+
portfolio: { id: resolution.manifest.id, name: resolution.manifest.name, owner: resolution.manifest.owner },
|
|
501
|
+
diagnostics: [],
|
|
502
|
+
members,
|
|
503
|
+
dependencies,
|
|
504
|
+
observedDependencies: [],
|
|
505
|
+
observedEvidence: { status: 'not-available', reason: 'No bounded cross-project Source Map dependency evidence is projected in V1.' },
|
|
506
|
+
attention,
|
|
507
|
+
activePersonas,
|
|
508
|
+
};
|
|
509
|
+
}
|