@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,239 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { TextDecoder } from 'node:util';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import YAML from 'yaml';
|
|
7
|
+
import { parseOrganisationBlueprintPack } from './organisation-blueprints.mjs';
|
|
8
|
+
import { parseDesignSystemPack } from './design-systems.mjs';
|
|
9
|
+
|
|
10
|
+
export const TEAM_HUB_RESOURCE_LIMITS = Object.freeze({
|
|
11
|
+
maximumFiles: 100,
|
|
12
|
+
maximumFileBytes: 256 * 1024,
|
|
13
|
+
maximumTotalBytes: 2 * 1024 * 1024,
|
|
14
|
+
maximumPackageBytes: (2 * 1024 * 1024) + (512 * 1024),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const digest = z.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
18
|
+
const resourceId = z.string().regex(/^org\.[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:[.-][a-z0-9]+)*$/);
|
|
19
|
+
const semanticVersion = z.string().regex(/^\d+\.\d+\.\d+$/);
|
|
20
|
+
const safeExtensions = new Set(['.md', '.yaml', '.yml', '.json', '.txt', '.toml', '.css']);
|
|
21
|
+
const textDecoder = new TextDecoder('utf-8', { fatal: true });
|
|
22
|
+
|
|
23
|
+
const fileSchema = z.object({
|
|
24
|
+
path: z.string().min(1).max(240),
|
|
25
|
+
size: z.number().int().min(0).max(TEAM_HUB_RESOURCE_LIMITS.maximumFileBytes),
|
|
26
|
+
digest,
|
|
27
|
+
content: z.string(),
|
|
28
|
+
}).strict();
|
|
29
|
+
|
|
30
|
+
const packageSchema = z.object({
|
|
31
|
+
schema: z.literal('ewai.team-hub-resource-package/v1'),
|
|
32
|
+
resource: z.object({
|
|
33
|
+
kind: z.enum(['organisation-blueprint', 'design-system']),
|
|
34
|
+
id: resourceId,
|
|
35
|
+
name: z.string().min(1).max(160),
|
|
36
|
+
description: z.string().max(1_000),
|
|
37
|
+
version: semanticVersion,
|
|
38
|
+
publisher: z.object({
|
|
39
|
+
id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
40
|
+
name: z.string().min(1).max(160),
|
|
41
|
+
}).strict(),
|
|
42
|
+
compatibility: z.object({ ewai: z.string().regex(/^\d+\.x$/) }).strict(),
|
|
43
|
+
}).strict(),
|
|
44
|
+
files: z.array(fileSchema).min(1).max(TEAM_HUB_RESOURCE_LIMITS.maximumFiles),
|
|
45
|
+
digest,
|
|
46
|
+
}).strict();
|
|
47
|
+
|
|
48
|
+
function resourceError(message, code = 'invalid-resource-package', status = 400) {
|
|
49
|
+
return Object.assign(new Error(message), { code, status });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fileDigest(bytes) {
|
|
53
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizedPackage(value) {
|
|
57
|
+
return {
|
|
58
|
+
schema: value.schema,
|
|
59
|
+
resource: value.resource,
|
|
60
|
+
files: [...value.files]
|
|
61
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
62
|
+
.map(({ path, size, digest: itemDigest, content }) => ({ path, size, digest: itemDigest, content })),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function teamHubResourcePackageDigest(value) {
|
|
67
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(normalizedPackage(value))).digest('hex')}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function safeResourcePath(value) {
|
|
71
|
+
const path = String(value ?? '').replaceAll('\\', '/');
|
|
72
|
+
const segments = path.split('/');
|
|
73
|
+
if (!path || path.length > 240 || isAbsolute(path) || segments.some((segment) => !segment || segment === '..' || segment.startsWith('.'))) {
|
|
74
|
+
throw resourceError(`Resource file path is not safe: ${value}`, 'unsafe-resource-path');
|
|
75
|
+
}
|
|
76
|
+
if (/[^\t\n\r\x20-\x7e]/.test(path) || !safeExtensions.has(extname(path).toLowerCase())) {
|
|
77
|
+
throw resourceError(`Resource file type or path is not supported: ${value}`, 'unsupported-resource-file');
|
|
78
|
+
}
|
|
79
|
+
return path;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readPackFile(root, source) {
|
|
83
|
+
const safePath = safeResourcePath(source);
|
|
84
|
+
const candidate = resolve(root, safePath);
|
|
85
|
+
const boundary = relative(root, candidate);
|
|
86
|
+
if (boundary.startsWith('..') || isAbsolute(boundary)) throw resourceError(`Resource file escapes the pack root: ${safePath}`, 'unsafe-resource-path');
|
|
87
|
+
if (!existsSync(candidate)) throw resourceError(`Resource file does not exist: ${safePath}`, 'missing-resource-file');
|
|
88
|
+
if (lstatSync(candidate).isSymbolicLink()) throw resourceError(`Resource files may not be symbolic links: ${safePath}`, 'unsafe-resource-symlink');
|
|
89
|
+
if (!statSync(candidate).isFile()) throw resourceError(`Resource path must be a file: ${safePath}`, 'invalid-resource-file');
|
|
90
|
+
const realRoot = realpathSync(root);
|
|
91
|
+
const realCandidate = realpathSync(candidate);
|
|
92
|
+
const realBoundary = relative(realRoot, realCandidate);
|
|
93
|
+
if (realBoundary.startsWith('..') || isAbsolute(realBoundary)) throw resourceError(`Resource file escapes the pack root: ${safePath}`, 'unsafe-resource-path');
|
|
94
|
+
const bytes = readFileSync(realCandidate);
|
|
95
|
+
if (bytes.length > TEAM_HUB_RESOURCE_LIMITS.maximumFileBytes) throw resourceError(`Resource file exceeds 256 KiB: ${safePath}`, 'resource-file-too-large');
|
|
96
|
+
let content;
|
|
97
|
+
try { content = textDecoder.decode(bytes); }
|
|
98
|
+
catch { throw resourceError(`Resource file must contain valid UTF-8 text: ${safePath}`, 'invalid-resource-text'); }
|
|
99
|
+
if (content.includes('\0')) throw resourceError(`Resource file contains binary NUL content: ${safePath}`, 'invalid-resource-text');
|
|
100
|
+
return { path: safePath, size: bytes.length, digest: fileDigest(bytes), content };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function referencedPaths(pack) {
|
|
104
|
+
if (pack.type === 'organisation') {
|
|
105
|
+
return pack.blueprint.modules.flatMap((module) => [
|
|
106
|
+
...module.standards.map(({ source }) => source),
|
|
107
|
+
...module.personas.map(({ source }) => source),
|
|
108
|
+
...module.policies.map(({ source }) => source),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
return pack.design_system.contributions.map(({ source }) => source);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateManifestBinding(value) {
|
|
115
|
+
const manifestFile = value.files.find(({ path }) => path === 'pack.yaml');
|
|
116
|
+
let manifest;
|
|
117
|
+
try { manifest = YAML.parse(manifestFile?.content ?? ''); }
|
|
118
|
+
catch { throw resourceError('Resource pack.yaml must contain valid YAML.', 'invalid-resource-manifest'); }
|
|
119
|
+
if (!manifest || manifest.schema !== 'ewai.pack/v1') throw resourceError('Resource pack.yaml must use ewai.pack/v1.', 'invalid-resource-manifest');
|
|
120
|
+
const expectedKind = manifest.type === 'organisation' ? 'organisation-blueprint' : manifest.type === 'design-system' ? 'design-system' : null;
|
|
121
|
+
if (!expectedKind || expectedKind !== value.resource.kind) throw resourceError('Resource kind disagrees with pack.yaml.', 'resource-manifest-mismatch');
|
|
122
|
+
for (const [field, expected] of [['id', manifest.id], ['name', manifest.name], ['version', manifest.version]]) {
|
|
123
|
+
if (value.resource[field] !== expected) throw resourceError(`Resource ${field} disagrees with pack.yaml.`, 'resource-manifest-mismatch');
|
|
124
|
+
}
|
|
125
|
+
if (value.resource.description !== String(manifest.description ?? '')) throw resourceError('Resource description disagrees with pack.yaml.', 'resource-manifest-mismatch');
|
|
126
|
+
|
|
127
|
+
const definition = manifest.type === 'organisation' ? manifest.blueprint : manifest.design_system;
|
|
128
|
+
if (!definition || value.resource.compatibility.ewai !== definition.compatibility?.ewai) {
|
|
129
|
+
throw resourceError('Resource compatibility disagrees with pack.yaml.', 'resource-manifest-mismatch');
|
|
130
|
+
}
|
|
131
|
+
const expectedPublisherId = manifest.type === 'organisation' ? definition.publisher?.id : String(manifest.id ?? '').split('.')[1];
|
|
132
|
+
if (value.resource.publisher.id !== expectedPublisherId) throw resourceError('Resource publisher disagrees with pack.yaml.', 'resource-manifest-mismatch');
|
|
133
|
+
if (manifest.type === 'organisation' && value.resource.publisher.name !== definition.publisher?.name) {
|
|
134
|
+
throw resourceError('Resource publisher name disagrees with pack.yaml.', 'resource-manifest-mismatch');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const references = manifest.type === 'organisation'
|
|
138
|
+
? (definition.modules ?? []).flatMap((module) => [
|
|
139
|
+
...(module.standards ?? []).map(({ source }) => source),
|
|
140
|
+
...(module.personas ?? []).map(({ source }) => source),
|
|
141
|
+
...(module.policies ?? []).map(({ source }) => source),
|
|
142
|
+
])
|
|
143
|
+
: (definition.contributions ?? []).map(({ source }) => source);
|
|
144
|
+
const expectedPaths = [...new Set(['pack.yaml', ...references.map(safeResourcePath)])].sort();
|
|
145
|
+
const actualPaths = value.files.map(({ path }) => path).sort();
|
|
146
|
+
if (JSON.stringify(expectedPaths) !== JSON.stringify(actualPaths)) {
|
|
147
|
+
throw resourceError('Resource file set disagrees with the files referenced by pack.yaml.', 'resource-manifest-mismatch');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function buildTeamHubResourcePackage(folder, options = {}) {
|
|
152
|
+
const root = resolve(String(folder ?? ''));
|
|
153
|
+
if (!String(folder ?? '').trim() || !existsSync(root) || !statSync(root).isDirectory()) {
|
|
154
|
+
throw resourceError('Resource publication requires an existing pack folder.', 'invalid-resource-folder');
|
|
155
|
+
}
|
|
156
|
+
if (lstatSync(root).isSymbolicLink()) throw resourceError('Resource pack folder must not be a symbolic link.', 'unsafe-resource-symlink');
|
|
157
|
+
const manifestPath = resolve(root, 'pack.yaml');
|
|
158
|
+
if (!existsSync(manifestPath) || basename(manifestPath) !== 'pack.yaml') throw resourceError('Resource pack requires pack.yaml.', 'missing-resource-manifest');
|
|
159
|
+
|
|
160
|
+
const organisation = parseOrganisationBlueprintPack(manifestPath, 'team-hub-candidate', {
|
|
161
|
+
limits: { manifestBytes: TEAM_HUB_RESOURCE_LIMITS.maximumFileBytes, itemBytes: TEAM_HUB_RESOURCE_LIMITS.maximumFileBytes, totalBytes: TEAM_HUB_RESOURCE_LIMITS.maximumTotalBytes },
|
|
162
|
+
});
|
|
163
|
+
const pack = organisation ?? parseDesignSystemPack(manifestPath, 'team-hub-candidate', {
|
|
164
|
+
limits: { manifestBytes: TEAM_HUB_RESOURCE_LIMITS.maximumFileBytes, itemBytes: TEAM_HUB_RESOURCE_LIMITS.maximumFileBytes, totalBytes: TEAM_HUB_RESOURCE_LIMITS.maximumTotalBytes, maximumFiles: TEAM_HUB_RESOURCE_LIMITS.maximumFiles },
|
|
165
|
+
});
|
|
166
|
+
if (!pack) throw resourceError('Team Hub supports only organisation Blueprint and design-system packs.', 'unsupported-resource-kind');
|
|
167
|
+
if (!pack.id.startsWith('org.')) throw resourceError('Team Hub resources must use an organisation-owned org.* ID.', 'unsupported-resource-id');
|
|
168
|
+
|
|
169
|
+
const publisherId = pack.type === 'organisation' ? pack.blueprint.publisher.id : pack.id.split('.')[1];
|
|
170
|
+
const publisherName = pack.type === 'organisation'
|
|
171
|
+
? pack.blueprint.publisher.name
|
|
172
|
+
: String(options.publisherName ?? '').trim();
|
|
173
|
+
if (!publisherName) throw resourceError('Design-system publication requires a publisher name.', 'missing-publisher-name');
|
|
174
|
+
const compatibility = pack.type === 'organisation' ? pack.blueprint.compatibility : pack.design_system.compatibility;
|
|
175
|
+
const paths = [...new Set(['pack.yaml', ...referencedPaths(pack)])].sort();
|
|
176
|
+
const files = paths.map((path) => readPackFile(root, path));
|
|
177
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
178
|
+
if (files.length > TEAM_HUB_RESOURCE_LIMITS.maximumFiles) throw resourceError('Resource package exceeds 100 files.', 'resource-file-limit');
|
|
179
|
+
if (total > TEAM_HUB_RESOURCE_LIMITS.maximumTotalBytes) throw resourceError('Resource package exceeds 2 MiB.', 'resource-package-too-large');
|
|
180
|
+
|
|
181
|
+
const value = {
|
|
182
|
+
schema: 'ewai.team-hub-resource-package/v1',
|
|
183
|
+
resource: {
|
|
184
|
+
kind: pack.type === 'organisation' ? 'organisation-blueprint' : 'design-system',
|
|
185
|
+
id: pack.id,
|
|
186
|
+
name: pack.name,
|
|
187
|
+
description: pack.description ?? '',
|
|
188
|
+
version: pack.version,
|
|
189
|
+
publisher: { id: publisherId, name: publisherName },
|
|
190
|
+
compatibility: { ...compatibility },
|
|
191
|
+
},
|
|
192
|
+
files,
|
|
193
|
+
digest: `sha256:${'0'.repeat(64)}`,
|
|
194
|
+
};
|
|
195
|
+
value.digest = teamHubResourcePackageDigest(value);
|
|
196
|
+
return validateTeamHubResourcePackage(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function validateTeamHubResourcePackage(input) {
|
|
200
|
+
let value;
|
|
201
|
+
try { value = packageSchema.parse(input); }
|
|
202
|
+
catch (error) {
|
|
203
|
+
const details = error.issues?.map((issue) => `${issue.path.join('.') || 'package'} ${issue.message}`).join('; ') ?? error.message;
|
|
204
|
+
throw resourceError(`Team Hub resource package is invalid: ${details}`);
|
|
205
|
+
}
|
|
206
|
+
if (value.resource.id.split('.')[1] !== value.resource.publisher.id) {
|
|
207
|
+
throw resourceError('Resource publisher ID must match the organisation segment in the resource ID.', 'publisher-mismatch');
|
|
208
|
+
}
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
let total = 0;
|
|
211
|
+
for (const file of value.files) {
|
|
212
|
+
const path = safeResourcePath(file.path);
|
|
213
|
+
if (seen.has(path)) throw resourceError(`Resource package repeats a file path: ${path}`, 'duplicate-resource-file');
|
|
214
|
+
seen.add(path);
|
|
215
|
+
let bytes;
|
|
216
|
+
try { bytes = Buffer.from(file.content, 'utf8'); textDecoder.decode(bytes); }
|
|
217
|
+
catch { throw resourceError(`Resource file must contain valid UTF-8 text: ${path}`, 'invalid-resource-text'); }
|
|
218
|
+
if (bytes.length !== file.size) throw resourceError(`Resource file size disagrees with its content: ${path}`, 'resource-size-mismatch');
|
|
219
|
+
if (fileDigest(bytes) !== file.digest) throw resourceError(`Resource file digest disagrees with its content: ${path}`, 'resource-digest-mismatch');
|
|
220
|
+
total += bytes.length;
|
|
221
|
+
}
|
|
222
|
+
if (!seen.has('pack.yaml')) throw resourceError('Resource package must contain pack.yaml.', 'missing-resource-manifest');
|
|
223
|
+
if (total > TEAM_HUB_RESOURCE_LIMITS.maximumTotalBytes) throw resourceError('Resource package exceeds 2 MiB.', 'resource-package-too-large');
|
|
224
|
+
validateManifestBinding(value);
|
|
225
|
+
const expected = teamHubResourcePackageDigest(value);
|
|
226
|
+
if (value.digest !== expected) throw resourceError('Resource package digest disagrees with its content.', 'resource-package-digest-mismatch');
|
|
227
|
+
return { ...value, files: [...value.files].sort((left, right) => left.path.localeCompare(right.path)) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function projectTeamHubResourceRelease(value) {
|
|
231
|
+
const pack = validateTeamHubResourcePackage(value);
|
|
232
|
+
return {
|
|
233
|
+
schema: 'ewai.team-hub-resource-release/v1',
|
|
234
|
+
...pack.resource,
|
|
235
|
+
digest: pack.digest,
|
|
236
|
+
fileCount: pack.files.length,
|
|
237
|
+
totalBytes: pack.files.reduce((sum, file) => sum + file.size, 0),
|
|
238
|
+
};
|
|
239
|
+
}
|
package/src/team-hub.mjs
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { loadProjectConfig } from './project.mjs';
|
|
8
|
+
|
|
9
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const packageVersion = JSON.parse(readFileSync(resolve(moduleDirectory, '../package.json'), 'utf8')).version;
|
|
11
|
+
|
|
12
|
+
export const TEAM_HUB_SNAPSHOT_SCHEMA = 'ewai.team-hub-snapshot/v1';
|
|
13
|
+
export const TEAM_HUB_ENVELOPE_SCHEMA = 'ewai.team-hub-envelope/v1';
|
|
14
|
+
export const TEAM_HUB_RECEIPT_SCHEMA = 'ewai.team-hub-receipt/v1';
|
|
15
|
+
export const TEAM_HUB_DISCLOSURE_SCHEMA = 'ewai.team-hub-disclosure/v1';
|
|
16
|
+
export const TEAM_HUB_AUTHORITY_NOTICE = 'Team Hub is an operational evidence projection. It cannot approve Build or Manual QA, accept risk, certify compliance, deploy or release.';
|
|
17
|
+
|
|
18
|
+
const digest = z.string().regex(/^sha256:[a-f0-9]{64}$/, 'must be a sha256 digest');
|
|
19
|
+
const isoDateTime = z.string().datetime({ offset: true });
|
|
20
|
+
const slug = z.string().min(1).max(120).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, 'must be a lower-case safe identifier');
|
|
21
|
+
const unsafeReference = /(?:\0|https?:\/\/|file:\/\/|(?:^|\s)\/(?:Users|home|private|tmp|var|etc)\/|[A-Za-z]:\\)/i;
|
|
22
|
+
const safeText = (maximum, label) => z.string().trim().min(1).max(maximum).refine(
|
|
23
|
+
(value) => !unsafeReference.test(value),
|
|
24
|
+
`${label} contains an unsafe path or remote reference`,
|
|
25
|
+
);
|
|
26
|
+
const attentionSchema = z.object({
|
|
27
|
+
code: slug,
|
|
28
|
+
severity: z.enum(['info', 'warning', 'blocking']),
|
|
29
|
+
summary: safeText(240, 'attention summary'),
|
|
30
|
+
}).strict();
|
|
31
|
+
const resourceSchema = z.object({
|
|
32
|
+
kind: z.enum(['organisation-blueprint', 'design-system', 'technology-pack', 'starter-pack']),
|
|
33
|
+
id: z.string().min(1).max(180).regex(/^(?:ewai|org)\.[a-z0-9.-]+$/),
|
|
34
|
+
version: z.string().min(1).max(40).regex(/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/),
|
|
35
|
+
digest,
|
|
36
|
+
}).strict();
|
|
37
|
+
const intentCountsSchema = z.object({
|
|
38
|
+
total: z.number().int().min(0).max(100_000),
|
|
39
|
+
draft: z.number().int().min(0).max(100_000),
|
|
40
|
+
ready: z.number().int().min(0).max(100_000),
|
|
41
|
+
inProgress: z.number().int().min(0).max(100_000),
|
|
42
|
+
completed: z.number().int().min(0).max(100_000),
|
|
43
|
+
blocked: z.number().int().min(0).max(100_000),
|
|
44
|
+
}).strict().superRefine((value, context) => {
|
|
45
|
+
const classified = value.draft + value.ready + value.inProgress + value.completed + value.blocked;
|
|
46
|
+
if (classified !== value.total) context.addIssue({ code: 'custom', message: 'Intent counts must add up to total.' });
|
|
47
|
+
});
|
|
48
|
+
const deliverySchema = z.object({
|
|
49
|
+
slug,
|
|
50
|
+
title: safeText(200, 'delivery title'),
|
|
51
|
+
status: safeText(80, 'delivery status'),
|
|
52
|
+
currentPhase: safeText(80, 'delivery phase'),
|
|
53
|
+
updatedAt: isoDateTime,
|
|
54
|
+
buildApproved: z.boolean(),
|
|
55
|
+
manualQa: safeText(80, 'Manual QA status'),
|
|
56
|
+
attention: z.array(attentionSchema).max(20),
|
|
57
|
+
}).strict();
|
|
58
|
+
const snapshotSchema = z.object({
|
|
59
|
+
schema: z.literal(TEAM_HUB_SNAPSHOT_SCHEMA),
|
|
60
|
+
project: z.object({ id: slug, name: safeText(200, 'project name') }).strict(),
|
|
61
|
+
generatedAt: isoDateTime,
|
|
62
|
+
ewaiVersion: z.string().regex(/^\d+\.\d+\.\d+(?:[-+][a-zA-Z0-9.-]+)?$/).max(40),
|
|
63
|
+
revision: z.object({ digest }).strict(),
|
|
64
|
+
intents: intentCountsSchema,
|
|
65
|
+
delivery: deliverySchema.nullable(),
|
|
66
|
+
resources: z.array(resourceSchema).max(25),
|
|
67
|
+
authorityNotice: z.literal(TEAM_HUB_AUTHORITY_NOTICE),
|
|
68
|
+
}).strict();
|
|
69
|
+
const envelopeSchema = z.object({
|
|
70
|
+
schema: z.literal(TEAM_HUB_ENVELOPE_SCHEMA),
|
|
71
|
+
idempotencyKey: digest,
|
|
72
|
+
snapshotDigest: digest,
|
|
73
|
+
snapshot: snapshotSchema,
|
|
74
|
+
}).strict();
|
|
75
|
+
|
|
76
|
+
function canonical(value) {
|
|
77
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
78
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function teamHubDigest(value) {
|
|
83
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parse(schema, value, label) {
|
|
87
|
+
const result = schema.safeParse(value);
|
|
88
|
+
if (result.success) return result.data;
|
|
89
|
+
const detail = result.error.issues.map((issue) => `${issue.path.join('.') || label}: ${issue.message}`).join('; ');
|
|
90
|
+
throw new Error(`${label} is invalid or contains an unrecognised field: ${detail}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateTeamHubSnapshot(value) {
|
|
94
|
+
return parse(snapshotSchema, value, 'Team Hub snapshot');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function validateTeamHubReceipt(value) {
|
|
98
|
+
return parse(z.object({
|
|
99
|
+
schema: z.literal(TEAM_HUB_RECEIPT_SCHEMA),
|
|
100
|
+
receiptId: z.string().regex(/^receipt-[a-zA-Z0-9-]{1,100}$/),
|
|
101
|
+
projectId: slug,
|
|
102
|
+
snapshotDigest: digest,
|
|
103
|
+
idempotencyKey: digest,
|
|
104
|
+
acceptedAt: isoDateTime,
|
|
105
|
+
replayed: z.boolean(),
|
|
106
|
+
}).strict(), value, 'Team Hub receipt');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildTeamHubEnvelope(snapshotInput) {
|
|
110
|
+
const snapshot = validateTeamHubSnapshot(snapshotInput);
|
|
111
|
+
const snapshotDigest = teamHubDigest(snapshot);
|
|
112
|
+
return { schema: TEAM_HUB_ENVELOPE_SCHEMA, idempotencyKey: snapshotDigest, snapshotDigest, snapshot };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function validateTeamHubEnvelope(value) {
|
|
116
|
+
const envelope = parse(envelopeSchema, value, 'Team Hub envelope');
|
|
117
|
+
const actual = teamHubDigest(envelope.snapshot);
|
|
118
|
+
if (envelope.snapshotDigest !== actual || envelope.idempotencyKey !== actual) {
|
|
119
|
+
throw new Error('Team Hub envelope digest or idempotency key disagrees with its snapshot.');
|
|
120
|
+
}
|
|
121
|
+
return envelope;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const disclosureFields = Object.freeze([
|
|
125
|
+
'project.id', 'project.name', 'generatedAt', 'ewaiVersion', 'revision.digest', 'intents',
|
|
126
|
+
'delivery', 'resources.kind', 'resources.id', 'resources.version', 'resources.digest', 'authorityNotice',
|
|
127
|
+
]);
|
|
128
|
+
const excludedCategories = Object.freeze([
|
|
129
|
+
'credentials', 'error-report-packages', 'local-paths', 'logs', 'persona-bodies', 'prompts-and-transcripts',
|
|
130
|
+
'repository-remotes', 'source-and-specs-bodies',
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
export function inspectTeamHubDisclosure() {
|
|
134
|
+
const contract = { schema: TEAM_HUB_SNAPSHOT_SCHEMA, fields: [...disclosureFields], excluded: [...excludedCategories] };
|
|
135
|
+
return {
|
|
136
|
+
schema: TEAM_HUB_DISCLOSURE_SCHEMA,
|
|
137
|
+
title: 'Exact project snapshot disclosure',
|
|
138
|
+
fields: contract.fields,
|
|
139
|
+
excluded: contract.excluded,
|
|
140
|
+
contractDigest: teamHubDigest(contract),
|
|
141
|
+
authorityNotice: TEAM_HUB_AUTHORITY_NOTICE,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function filesUnder(root, suffix, maximum = 10_000) {
|
|
146
|
+
if (!existsSync(root) || lstatSync(root).isSymbolicLink()) return [];
|
|
147
|
+
const found = [];
|
|
148
|
+
const queue = [root];
|
|
149
|
+
while (queue.length && found.length < maximum) {
|
|
150
|
+
const current = queue.shift();
|
|
151
|
+
for (const entry of readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
152
|
+
if (entry.isSymbolicLink()) continue;
|
|
153
|
+
const path = resolve(current, entry.name);
|
|
154
|
+
if (entry.isDirectory()) queue.push(path);
|
|
155
|
+
if (entry.isFile() && entry.name.endsWith(suffix) && statSync(path).size <= 512 * 1024) found.push(path);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return found;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function intentCounts(specsRoot) {
|
|
162
|
+
const counts = { total: 0, draft: 0, ready: 0, inProgress: 0, completed: 0, blocked: 0 };
|
|
163
|
+
for (const path of filesUnder(resolve(specsRoot, '2.Purpose/intents'), '.json')) {
|
|
164
|
+
try {
|
|
165
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
166
|
+
if (value.schema !== 'ewai.intent-state/v1') continue;
|
|
167
|
+
counts.total += 1;
|
|
168
|
+
const status = String(value.status ?? '').toLowerCase();
|
|
169
|
+
if (status === 'completed' || status === 'done') counts.completed += 1;
|
|
170
|
+
else if (status === 'blocked') counts.blocked += 1;
|
|
171
|
+
else if (status === 'in-progress' || status === 'in_progress' || value.deliveryStatus === 'in-progress') counts.inProgress += 1;
|
|
172
|
+
else if (status === 'ready' || status === 'approved') counts.ready += 1;
|
|
173
|
+
else counts.draft += 1;
|
|
174
|
+
} catch {
|
|
175
|
+
// Invalid intent state is not promoted into a Hub snapshot.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return counts;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function latestDelivery(specsRoot) {
|
|
182
|
+
const records = [];
|
|
183
|
+
for (const path of filesUnder(resolve(specsRoot, '6.Build'), 'delivery-state.json', 500)) {
|
|
184
|
+
try {
|
|
185
|
+
const state = JSON.parse(readFileSync(path, 'utf8'));
|
|
186
|
+
if (state.schema !== 'ewai.delivery-state/v1' || !state.updatedAt) continue;
|
|
187
|
+
const manualQa = (state.humanGates ?? []).find((gate) => gate.id === 'manual-qa');
|
|
188
|
+
const attention = [];
|
|
189
|
+
if (state.blockedReason || state.status === 'blocked') attention.push({ code: 'delivery-blocked', severity: 'blocking', summary: 'The latest delivery reports a blocker.' });
|
|
190
|
+
if ((manualQa?.status ?? 'pending') !== 'approved') attention.push({ code: 'manual-qa-pending', severity: 'info', summary: 'Manual QA remains pending.' });
|
|
191
|
+
records.push({
|
|
192
|
+
slug: String(state.slug ?? '').slice(0, 120),
|
|
193
|
+
title: String(state.intent?.title ?? state.slug ?? 'Delivery').slice(0, 200),
|
|
194
|
+
status: String(state.status ?? 'unknown').slice(0, 80),
|
|
195
|
+
currentPhase: String(state.currentPhase ?? 'unknown').slice(0, 80),
|
|
196
|
+
updatedAt: String(state.updatedAt), buildApproved: Boolean(state.approvals?.build),
|
|
197
|
+
manualQa: String(manualQa?.status ?? 'pending').slice(0, 80), attention,
|
|
198
|
+
});
|
|
199
|
+
} catch {
|
|
200
|
+
// Invalid delivery state is not promoted into a Hub snapshot.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return records.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function repositoryRevision(projectRoot) {
|
|
207
|
+
try {
|
|
208
|
+
const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: projectRoot, encoding: 'utf8', timeout: 2_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
209
|
+
return teamHubDigest({ head });
|
|
210
|
+
} catch {
|
|
211
|
+
return teamHubDigest({ head: 'unavailable' });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function selectedResources(config) {
|
|
216
|
+
const resources = [];
|
|
217
|
+
const blueprint = config.blueprints?.organisation?.root;
|
|
218
|
+
if (blueprint?.id && blueprint?.version && blueprint?.digest) resources.push({ kind: 'organisation-blueprint', id: blueprint.id, version: blueprint.version, digest: blueprint.digest });
|
|
219
|
+
const design = config.design_system?.root;
|
|
220
|
+
const designDigest = config.design_system?.effective_digest ?? design?.digest;
|
|
221
|
+
if (design?.id && design?.version && designDigest) resources.push({ kind: 'design-system', id: design.id, version: design.version, digest: designDigest });
|
|
222
|
+
return resources.slice(0, 25);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function buildTeamHubSnapshot(projectRoot, options = {}) {
|
|
226
|
+
const { config, paths } = loadProjectConfig(projectRoot);
|
|
227
|
+
const snapshot = {
|
|
228
|
+
schema: TEAM_HUB_SNAPSHOT_SCHEMA,
|
|
229
|
+
project: {
|
|
230
|
+
id: options.projectId ?? 'local-project',
|
|
231
|
+
name: config.project?.name ?? 'EWAI project',
|
|
232
|
+
},
|
|
233
|
+
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
234
|
+
ewaiVersion: packageVersion,
|
|
235
|
+
revision: { digest: options.revisionDigest ?? repositoryRevision(paths.projectRoot) },
|
|
236
|
+
intents: options.intents ?? intentCounts(paths.specsRoot),
|
|
237
|
+
delivery: options.delivery === undefined ? latestDelivery(paths.specsRoot) : options.delivery,
|
|
238
|
+
resources: options.resources ?? selectedResources(config),
|
|
239
|
+
authorityNotice: TEAM_HUB_AUTHORITY_NOTICE,
|
|
240
|
+
};
|
|
241
|
+
return validateTeamHubSnapshot(snapshot);
|
|
242
|
+
}
|