@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,703 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
statSync,
|
|
12
|
+
writeFileSync
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
16
|
+
import { projectPaths } from './paths.mjs';
|
|
17
|
+
import {
|
|
18
|
+
listPersonas,
|
|
19
|
+
personalPersonaRoot,
|
|
20
|
+
projectPersonaRoot
|
|
21
|
+
} from './personas.mjs';
|
|
22
|
+
import { selectContextualPersonas } from './runtime/persona-engagement.mjs';
|
|
23
|
+
import { publishLifecycleEventSafely } from './runtime/lifecycle-hooks.mjs';
|
|
24
|
+
|
|
25
|
+
export const MEETING_EVIDENCE_DISCLAIMER = 'Security validation is evidence, not certification or proof that this system is secure. Tools can miss vulnerabilities and produce false positives. A qualified human must review the scope, findings, limitations and residual risk before release.';
|
|
26
|
+
|
|
27
|
+
const SOURCE_SCHEMA = 'ewai.meeting-source-private/v1';
|
|
28
|
+
const REGISTRATION_SCHEMA = 'ewai.meeting-source-registration/v1';
|
|
29
|
+
const EXTRACTION_SCHEMA = 'ewai.meeting-extraction-contract/v1';
|
|
30
|
+
const WORKSPACE_SCHEMA = 'ewai.meeting-evidence-workspace/v1';
|
|
31
|
+
const CANDIDATE_SCHEMA = 'ewai.meeting-candidate-bundle/v1';
|
|
32
|
+
const REVIEW_SCHEMA = 'ewai.meeting-review/v1';
|
|
33
|
+
const EVIDENCE_SCHEMA = 'ewai.meeting-evidence/v1';
|
|
34
|
+
const sourceIdPattern = /^meeting\.[a-z0-9][a-z0-9-]{0,59}\.[a-f0-9]{12}$/;
|
|
35
|
+
const classifications = new Set(['public', 'internal', 'confidential', 'restricted']);
|
|
36
|
+
const cloudPolicies = new Set(['allowed', 'denied', 'unknown']);
|
|
37
|
+
const supportedExtensions = new Set(['.md', '.markdown', '.txt', '.text', '.vtt', '.srt']);
|
|
38
|
+
const allowedCandidateTypes = [
|
|
39
|
+
'task',
|
|
40
|
+
'decision',
|
|
41
|
+
'process',
|
|
42
|
+
'risk',
|
|
43
|
+
'system',
|
|
44
|
+
'policy',
|
|
45
|
+
'requirement',
|
|
46
|
+
'assumption',
|
|
47
|
+
'open-question'
|
|
48
|
+
];
|
|
49
|
+
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
|
50
|
+
const MAX_SOURCE_LINES = 20_000;
|
|
51
|
+
const candidateIdPattern = /^MEC-[0-9]{3,}$/;
|
|
52
|
+
const digestPattern = /^[a-f0-9]{64}$/;
|
|
53
|
+
const reviewDecisions = new Set(['accepted', 'rejected', 'amended', 'deferred']);
|
|
54
|
+
|
|
55
|
+
function normalized(path) {
|
|
56
|
+
return path.split(sep).join('/');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function inside(parent, child) {
|
|
60
|
+
const path = relative(parent, child);
|
|
61
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function slugify(value) {
|
|
65
|
+
return String(value ?? '')
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
68
|
+
.replace(/^-+|-+$/g, '')
|
|
69
|
+
.slice(0, 60) || 'source';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cleanText(value, label, max = 200, required = true) {
|
|
73
|
+
const text = String(value ?? '').trim();
|
|
74
|
+
if (required && !text) throw new Error(`Meeting evidence requires ${label}`);
|
|
75
|
+
if (text.length > max) throw new Error(`Meeting evidence ${label} exceeds ${max} characters`);
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sourceRoot(paths) {
|
|
80
|
+
return resolve(paths.runtimeRoot, 'meeting-evidence/sources');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sourceRecordPath(paths, sourceId) {
|
|
84
|
+
if (!sourceIdPattern.test(String(sourceId ?? ''))) throw new Error(`Invalid meeting source ID: ${sourceId}`);
|
|
85
|
+
const root = sourceRoot(paths);
|
|
86
|
+
const path = resolve(root, `${sourceId}.json`);
|
|
87
|
+
if (!inside(root, path)) throw new Error('Unsafe meeting source path');
|
|
88
|
+
return path;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function atomicJson(path, value) {
|
|
92
|
+
mkdirSync(resolve(path, '..'), { recursive: true });
|
|
93
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
94
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
95
|
+
renameSync(temporary, path);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sha256(value) {
|
|
99
|
+
return createHash('sha256').update(value).digest('hex');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function canonicalDigest(value) {
|
|
103
|
+
return sha256(JSON.stringify(value));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function safeRelative(projectRoot, path) {
|
|
107
|
+
const value = normalized(relative(projectRoot, path));
|
|
108
|
+
if (!value || value.startsWith('../') || isAbsolute(value)) throw new Error('Meeting evidence path escapes the project');
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function contentFacts(path, stats = statSync(path)) {
|
|
113
|
+
if (stats.size > MAX_SOURCE_BYTES) throw new Error('Meeting source exceeds the 2 MiB limit');
|
|
114
|
+
const content = readFileSync(path);
|
|
115
|
+
const text = content.toString('utf8');
|
|
116
|
+
const lineCount = text ? text.split(/\r?\n/).length - (/\r?\n$/.test(text) ? 1 : 0) : 0;
|
|
117
|
+
if (lineCount > MAX_SOURCE_LINES) throw new Error('Meeting source exceeds the 20,000 lines limit');
|
|
118
|
+
return {
|
|
119
|
+
size: stats.size,
|
|
120
|
+
lineCount,
|
|
121
|
+
digest: createHash('sha256').update(content).digest('hex'),
|
|
122
|
+
modifiedAt: stats.mtime.toISOString()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readPrivateSource(paths, sourceId) {
|
|
127
|
+
const path = sourceRecordPath(paths, sourceId);
|
|
128
|
+
if (!existsSync(path)) throw new Error(`Unknown meeting source: ${sourceId}`);
|
|
129
|
+
const record = JSON.parse(readFileSync(path, 'utf8'));
|
|
130
|
+
if (record.schema !== SOURCE_SCHEMA || record.sourceId !== sourceId) {
|
|
131
|
+
throw new Error(`Invalid meeting source record: ${sourceId}`);
|
|
132
|
+
}
|
|
133
|
+
return record;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function reviewRoot(paths) {
|
|
137
|
+
return resolve(paths.runtimeRoot, 'meeting-evidence/reviews');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function reviewRecordPath(paths, sourceId) {
|
|
141
|
+
const root = reviewRoot(paths);
|
|
142
|
+
const path = resolve(root, `${sourceId}.json`);
|
|
143
|
+
if (!sourceIdPattern.test(String(sourceId ?? '')) || !inside(root, path)) throw new Error(`Invalid meeting source ID: ${sourceId}`);
|
|
144
|
+
return path;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readReview(paths, sourceId) {
|
|
148
|
+
const path = reviewRecordPath(paths, sourceId);
|
|
149
|
+
if (!existsSync(path)) throw new Error(`Meeting source has no current named review: ${sourceId}`);
|
|
150
|
+
const review = JSON.parse(readFileSync(path, 'utf8'));
|
|
151
|
+
if (review.schema !== REVIEW_SCHEMA || review.sourceId !== sourceId) throw new Error(`Invalid meeting review record: ${sourceId}`);
|
|
152
|
+
const { reviewDigest, ...payload } = review;
|
|
153
|
+
if (!digestPattern.test(String(reviewDigest ?? '')) || canonicalDigest(payload) !== reviewDigest) {
|
|
154
|
+
throw new Error(`Meeting review digest is invalid: ${sourceId}`);
|
|
155
|
+
}
|
|
156
|
+
return review;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function evidencePaths(paths, sourceId) {
|
|
160
|
+
const root = resolve(paths.specsRoot, '3.Evidence/meeting-evidence', sourceId);
|
|
161
|
+
if (!inside(paths.specsRoot, root)) throw new Error('Unsafe meeting evidence destination');
|
|
162
|
+
return {
|
|
163
|
+
root,
|
|
164
|
+
json: resolve(root, 'evidence.json'),
|
|
165
|
+
markdown: resolve(root, 'evidence.md')
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function transactionPaths(paths, sourceId) {
|
|
170
|
+
const root = resolve(paths.runtimeRoot, 'meeting-evidence/transactions');
|
|
171
|
+
return { root, journal: resolve(root, `${sourceId}.json`) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function cleanPromotionTransaction(paths, sourceId) {
|
|
175
|
+
const transaction = transactionPaths(paths, sourceId);
|
|
176
|
+
if (!existsSync(transaction.journal)) return;
|
|
177
|
+
const journal = JSON.parse(readFileSync(transaction.journal, 'utf8'));
|
|
178
|
+
const targets = evidencePaths(paths, sourceId);
|
|
179
|
+
for (const staged of [journal.jsonStage, journal.markdownStage]) {
|
|
180
|
+
const path = resolve(String(staged ?? ''));
|
|
181
|
+
if (inside(targets.root, path) && path.endsWith('.tmp')) rmSync(path, { force: true });
|
|
182
|
+
}
|
|
183
|
+
if (journal.phase !== 'committed') {
|
|
184
|
+
rmSync(targets.json, { force: true });
|
|
185
|
+
rmSync(targets.markdown, { force: true });
|
|
186
|
+
}
|
|
187
|
+
rmSync(transaction.journal, { force: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function safeSource(record, facts) {
|
|
191
|
+
const current = facts ?? sourceCurrentFacts(record);
|
|
192
|
+
const freshness = current.missing
|
|
193
|
+
? 'missing'
|
|
194
|
+
: current.digest === record.digest && current.size === record.size
|
|
195
|
+
? 'current'
|
|
196
|
+
: 'stale';
|
|
197
|
+
return {
|
|
198
|
+
sourceId: record.sourceId,
|
|
199
|
+
label: record.label,
|
|
200
|
+
extension: record.extension,
|
|
201
|
+
classification: record.classification,
|
|
202
|
+
cloudProcessing: record.cloudProcessing,
|
|
203
|
+
size: record.size,
|
|
204
|
+
lineCount: record.lineCount,
|
|
205
|
+
digest: record.digest,
|
|
206
|
+
registeredAt: record.registeredAt,
|
|
207
|
+
modifiedAt: record.modifiedAt,
|
|
208
|
+
freshness
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sourceCurrentFacts(record) {
|
|
213
|
+
if (!existsSync(record.sourcePath)) return { missing: true };
|
|
214
|
+
try {
|
|
215
|
+
const stats = statSync(record.sourcePath);
|
|
216
|
+
if (!stats.isFile()) return { missing: true };
|
|
217
|
+
return contentFacts(record.sourcePath, stats);
|
|
218
|
+
} catch {
|
|
219
|
+
return { missing: true };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function inferTier(persona) {
|
|
224
|
+
const path = normalized(String(persona.path ?? ''));
|
|
225
|
+
if (persona.tier) return persona.tier;
|
|
226
|
+
if (path.includes('/premium-personas/')) return 'premium';
|
|
227
|
+
if (path.includes('/1.Scope/personas/project/')) return 'project';
|
|
228
|
+
if (path.includes('/.ewai/personas/')) return 'personal';
|
|
229
|
+
return 'core';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function installedPersonaCatalogue(projectRoot) {
|
|
233
|
+
const roots = [
|
|
234
|
+
resolve(import.meta.dirname, '../packs/personas/core/personas'),
|
|
235
|
+
resolve(homedir(), '.ewai/packs/ewai.personas.professional/premium-personas'),
|
|
236
|
+
personalPersonaRoot(),
|
|
237
|
+
projectPersonaRoot(projectRoot)
|
|
238
|
+
];
|
|
239
|
+
const ranks = { project: 4, premium: 3, personal: 2, core: 1 };
|
|
240
|
+
const found = new Map();
|
|
241
|
+
for (const persona of listPersonas(roots)) {
|
|
242
|
+
const candidate = { ...persona, tier: inferTier(persona) };
|
|
243
|
+
const existing = found.get(candidate.id);
|
|
244
|
+
if (!existing || ranks[candidate.tier] > ranks[existing.tier]) found.set(candidate.id, candidate);
|
|
245
|
+
}
|
|
246
|
+
return [...found.values()];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function contextualPersonas(projectRoot, source, options = {}) {
|
|
250
|
+
const focus = Array.isArray(options.focus)
|
|
251
|
+
? options.focus.map((item) => String(item).trim()).filter(Boolean)
|
|
252
|
+
: String(options.focus ?? '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
253
|
+
const signals = focus.length
|
|
254
|
+
? focus
|
|
255
|
+
: ['meeting', 'evidence', 'provenance', 'review', source.classification, source.cloudProcessing];
|
|
256
|
+
const catalogue = Array.isArray(options.personaCatalogue)
|
|
257
|
+
? options.personaCatalogue
|
|
258
|
+
: installedPersonaCatalogue(projectRoot);
|
|
259
|
+
return selectContextualPersonas({
|
|
260
|
+
signals,
|
|
261
|
+
context: {
|
|
262
|
+
source: {
|
|
263
|
+
classification: source.classification,
|
|
264
|
+
cloudProcessing: source.cloudProcessing,
|
|
265
|
+
freshness: source.freshness
|
|
266
|
+
},
|
|
267
|
+
focus
|
|
268
|
+
},
|
|
269
|
+
personaCatalogue: catalogue.map((persona) => ({ ...persona, tier: inferTier(persona) })),
|
|
270
|
+
contextLabel: 'meeting evidence extraction',
|
|
271
|
+
limit: options.personaLimit ?? 4
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function candidateContract(source) {
|
|
276
|
+
return {
|
|
277
|
+
schema: CANDIDATE_SCHEMA,
|
|
278
|
+
schemaPath: 'config/meeting-evidence-candidate.schema.json',
|
|
279
|
+
sourceId: source.sourceId,
|
|
280
|
+
sourceDigest: source.digest,
|
|
281
|
+
sourceLineCount: source.lineCount,
|
|
282
|
+
allowedTypes: [...allowedCandidateTypes],
|
|
283
|
+
confidenceLabels: ['low', 'medium', 'high'],
|
|
284
|
+
requiredBoundaries: ['observedStatement', 'interpretation', 'lineAnchors', 'confidence'],
|
|
285
|
+
rawSourceExcerptsAllowed: false,
|
|
286
|
+
rawModelOutputAllowed: false
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normaliseCandidate(raw, source) {
|
|
291
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Each meeting candidate must be an object');
|
|
292
|
+
const allowed = new Set(['id', 'type', 'observedStatement', 'interpretation', 'lineAnchors', 'confidence']);
|
|
293
|
+
for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`Meeting candidate field is not allowed: ${key}`);
|
|
294
|
+
const id = cleanText(raw.id, 'candidate ID', 30);
|
|
295
|
+
if (!candidateIdPattern.test(id)) throw new Error(`Invalid meeting candidate ID: ${id}`);
|
|
296
|
+
const type = cleanText(raw.type, 'candidate type', 40);
|
|
297
|
+
if (!allowedCandidateTypes.includes(type)) throw new Error(`Unsupported meeting candidate type: ${type}`);
|
|
298
|
+
const observedStatement = cleanText(raw.observedStatement, 'candidate observed statement', 600);
|
|
299
|
+
const interpretation = cleanText(raw.interpretation, 'candidate interpretation', 1000);
|
|
300
|
+
const confidence = cleanText(raw.confidence, 'candidate confidence', 20);
|
|
301
|
+
if (!['low', 'medium', 'high'].includes(confidence)) throw new Error(`Unsupported meeting candidate confidence: ${confidence}`);
|
|
302
|
+
if (!Array.isArray(raw.lineAnchors) || !raw.lineAnchors.length || raw.lineAnchors.length > 20) {
|
|
303
|
+
throw new Error(`${id} requires one to 20 line anchors`);
|
|
304
|
+
}
|
|
305
|
+
const lineAnchors = raw.lineAnchors.map((anchor) => {
|
|
306
|
+
const start = Number(anchor?.start);
|
|
307
|
+
const end = Number(anchor?.end);
|
|
308
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end < start || end > source.lineCount) {
|
|
309
|
+
throw new Error(`${id} has an invalid line anchor`);
|
|
310
|
+
}
|
|
311
|
+
return { start, end };
|
|
312
|
+
});
|
|
313
|
+
return { id, type, observedStatement, interpretation, lineAnchors, confidence };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function normaliseBundle(raw, source) {
|
|
317
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Meeting candidate bundle must be an object');
|
|
318
|
+
const allowed = new Set(['schema', 'sourceId', 'sourceDigest', 'candidates']);
|
|
319
|
+
for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`Meeting candidate bundle field is not allowed: ${key}`);
|
|
320
|
+
if (raw.schema !== CANDIDATE_SCHEMA) throw new Error(`Meeting candidate bundle schema must be ${CANDIDATE_SCHEMA}`);
|
|
321
|
+
if (raw.sourceId !== source.sourceId) throw new Error('Meeting candidate bundle source does not match');
|
|
322
|
+
if (raw.sourceDigest !== source.digest) throw new Error('Meeting candidate bundle source digest is stale');
|
|
323
|
+
if (!Array.isArray(raw.candidates) || !raw.candidates.length || raw.candidates.length > 500) {
|
|
324
|
+
throw new Error('Meeting candidate bundle must contain one to 500 candidates');
|
|
325
|
+
}
|
|
326
|
+
const candidates = raw.candidates.map((candidate) => normaliseCandidate(candidate, source));
|
|
327
|
+
if (new Set(candidates.map(({ id }) => id)).size !== candidates.length) throw new Error('Meeting candidate bundle contains a duplicate candidate ID');
|
|
328
|
+
return { schema: CANDIDATE_SCHEMA, sourceId: source.sourceId, sourceDigest: source.digest, candidates };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normaliseDispositions(raw, candidates) {
|
|
332
|
+
if (!Array.isArray(raw) || raw.length !== candidates.length) throw new Error('Meeting review must dispose of every candidate exactly once');
|
|
333
|
+
const known = new Set(candidates.map(({ id }) => id));
|
|
334
|
+
const dispositions = raw.map((item) => {
|
|
335
|
+
const candidateId = cleanText(item?.candidateId, 'review candidate ID', 30);
|
|
336
|
+
if (!known.has(candidateId)) throw new Error(`Meeting review references an unknown candidate: ${candidateId}`);
|
|
337
|
+
const decision = cleanText(item?.decision, 'review decision', 20);
|
|
338
|
+
if (!reviewDecisions.has(decision)) throw new Error(`Unsupported meeting review decision: ${decision}`);
|
|
339
|
+
const replacementText = cleanText(item?.replacementText, 'amendment replacement text', 1000, false);
|
|
340
|
+
const rationale = cleanText(item?.rationale, 'review rationale', 1000, false);
|
|
341
|
+
if (decision === 'amended' && (!replacementText || !rationale)) throw new Error(`${candidateId} amendment requires replacement text and rationale`);
|
|
342
|
+
if (decision !== 'amended' && replacementText) throw new Error(`${candidateId} replacement text is allowed only for an amendment`);
|
|
343
|
+
return { candidateId, decision, ...(replacementText ? { replacementText } : {}), ...(rationale ? { rationale } : {}) };
|
|
344
|
+
});
|
|
345
|
+
if (new Set(dispositions.map(({ candidateId }) => candidateId)).size !== dispositions.length) {
|
|
346
|
+
throw new Error('Meeting review must dispose of every candidate exactly once');
|
|
347
|
+
}
|
|
348
|
+
return dispositions;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function renderEvidence(record) {
|
|
352
|
+
const items = record.evidence.map((item) => `### ${item.id} — ${item.type}\n\n- **Statement:** ${item.statement}\n- **Interpretation:** ${item.interpretation}\n- **Confidence:** ${item.confidence}\n- **Source lines:** ${item.lineAnchors.map(({ start, end }) => start === end ? `${start}` : `${start}-${end}`).join(', ')}\n- **Review decision:** ${item.reviewDecision}\n`).join('\n');
|
|
353
|
+
return `# Meeting Evidence — ${record.source.label}\n\n**Schema:** \`${record.schema}\` \n**Source ID:** \`${record.source.sourceId}\` \n**Source digest:** \`${record.source.digest}\` \n**Reviewed by:** ${record.review.reviewedBy} \n**Approved by:** ${record.promotion.approvedBy} \n**Approved at:** ${record.promotion.approvedAt} \n**Evidence digest:** \`${record.evidenceDigest}\`\n\n## Reviewed evidence\n\n${items}\n## Authority boundary\n\nThis pack records reviewed meeting evidence only. It does not create an intent, task, requirement, policy, architecture decision, delivery approval, accepted risk or release authority.\n\n${MEETING_EVIDENCE_DISCLAIMER}\n`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function assertInitialized(paths) {
|
|
357
|
+
if (!existsSync(paths.configPath)) throw new Error('Initialize EWAI before registering meeting evidence');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function registerMeetingSource(projectRoot, file, options = {}) {
|
|
361
|
+
if (!options.confirmed) throw new Error('Meeting source registration requires explicit user confirmation');
|
|
362
|
+
const paths = projectPaths(projectRoot);
|
|
363
|
+
assertInitialized(paths);
|
|
364
|
+
|
|
365
|
+
const rawPath = cleanText(file, 'source file', 4096);
|
|
366
|
+
if (rawPath.split(/[\\/]/).includes('..')) throw new Error('Meeting source path traversal is not allowed');
|
|
367
|
+
const candidate = resolve(rawPath);
|
|
368
|
+
if (!existsSync(candidate)) throw new Error('Meeting source is not a readable regular file');
|
|
369
|
+
const linkStats = lstatSync(candidate);
|
|
370
|
+
if (linkStats.isSymbolicLink()) throw new Error('Meeting source must not be a symbolic link');
|
|
371
|
+
if (!linkStats.isFile()) throw new Error('Meeting source must be a readable regular file');
|
|
372
|
+
const sourcePath = realpathSync(candidate);
|
|
373
|
+
const extension = extname(sourcePath).toLowerCase();
|
|
374
|
+
if (!supportedExtensions.has(extension)) throw new Error(`Unsupported meeting source type: ${extension || '(none)'}`);
|
|
375
|
+
|
|
376
|
+
const classification = options.classification ?? 'confidential';
|
|
377
|
+
if (!classifications.has(classification)) throw new Error(`Unsupported meeting source classification: ${classification}`);
|
|
378
|
+
const cloudProcessing = options.cloudProcessing ?? 'unknown';
|
|
379
|
+
if (!cloudPolicies.has(cloudProcessing)) throw new Error(`Unsupported meeting source cloud-processing policy: ${cloudProcessing}`);
|
|
380
|
+
|
|
381
|
+
const facts = contentFacts(sourcePath, statSync(sourcePath));
|
|
382
|
+
const existingSource = existsSync(sourceRoot(paths))
|
|
383
|
+
? readdirSync(sourceRoot(paths), { withFileTypes: true })
|
|
384
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
385
|
+
.map((entry) => {
|
|
386
|
+
try {
|
|
387
|
+
return JSON.parse(readFileSync(resolve(sourceRoot(paths), entry.name), 'utf8'));
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
.find((record) => record?.schema === SOURCE_SCHEMA && record.sourcePath === sourcePath)
|
|
393
|
+
: null;
|
|
394
|
+
if (existingSource) throw new Error(`Meeting source is already registered: ${existingSource.sourceId}`);
|
|
395
|
+
const project = realpathSync(paths.projectRoot);
|
|
396
|
+
const identity = createHash('sha256').update(`${project}\0${sourcePath}`).digest('hex').slice(0, 12);
|
|
397
|
+
const requestedLabel = String(options.label ?? '').trim();
|
|
398
|
+
const label = requestedLabel
|
|
399
|
+
? cleanText(requestedLabel, 'source label', 160)
|
|
400
|
+
: `Meeting source ${identity.slice(0, 6)}`;
|
|
401
|
+
const sourceId = `meeting.${slugify(label)}.${identity}`;
|
|
402
|
+
const path = sourceRecordPath(paths, sourceId);
|
|
403
|
+
if (existsSync(path)) throw new Error(`Meeting source is already registered: ${sourceId}`);
|
|
404
|
+
|
|
405
|
+
const registeredAt = new Date(options.now ?? Date.now()).toISOString();
|
|
406
|
+
const record = {
|
|
407
|
+
schema: SOURCE_SCHEMA,
|
|
408
|
+
sourceId,
|
|
409
|
+
label,
|
|
410
|
+
sourcePath,
|
|
411
|
+
extension,
|
|
412
|
+
classification,
|
|
413
|
+
cloudProcessing,
|
|
414
|
+
...facts,
|
|
415
|
+
registeredAt
|
|
416
|
+
};
|
|
417
|
+
atomicJson(path, record);
|
|
418
|
+
return {
|
|
419
|
+
schema: REGISTRATION_SCHEMA,
|
|
420
|
+
source: safeSource(record, facts),
|
|
421
|
+
notices: [
|
|
422
|
+
'Raw meeting material remains in its original file and is not copied into SPECS or the runtime database.',
|
|
423
|
+
'Personas provide advisory lenses; they do not approve evidence or replace accountable human review.',
|
|
424
|
+
MEETING_EVIDENCE_DISCLAIMER
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function prepareMeetingExtraction(projectRoot, sourceId, options = {}) {
|
|
430
|
+
const paths = projectPaths(projectRoot);
|
|
431
|
+
assertInitialized(paths);
|
|
432
|
+
const record = readPrivateSource(paths, sourceId);
|
|
433
|
+
const facts = sourceCurrentFacts(record);
|
|
434
|
+
const source = safeSource(record, facts);
|
|
435
|
+
if (source.freshness !== 'current') throw new Error(`Meeting source is ${source.freshness}; register or review the current source before extraction`);
|
|
436
|
+
const permitted = source.cloudProcessing === 'allowed';
|
|
437
|
+
const activePersonas = contextualPersonas(paths.projectRoot, source, options);
|
|
438
|
+
return {
|
|
439
|
+
schema: EXTRACTION_SCHEMA,
|
|
440
|
+
source,
|
|
441
|
+
processing: {
|
|
442
|
+
permitted,
|
|
443
|
+
route: permitted ? 'host-model-with-skill' : 'manual-local-handoff',
|
|
444
|
+
reason: permitted
|
|
445
|
+
? 'Cloud processing is explicitly allowed for this registered source.'
|
|
446
|
+
: `Cloud processing is ${source.cloudProcessing}; host-model source inspection is not permitted.`
|
|
447
|
+
},
|
|
448
|
+
candidateContract: candidateContract(source),
|
|
449
|
+
activePersonas,
|
|
450
|
+
authority: {
|
|
451
|
+
personasAreAdvisory: true,
|
|
452
|
+
namedReviewRequired: true,
|
|
453
|
+
promotionRequiresSeparateApproval: true,
|
|
454
|
+
downstreamCanonicalAuthoringAllowed: false
|
|
455
|
+
},
|
|
456
|
+
notices: [
|
|
457
|
+
'Do not copy raw source content, source excerpts, prompts or model output into the candidate record.',
|
|
458
|
+
'The standard host model and project/core personas are complete; installed premium and personal personas are optional enrichment only.',
|
|
459
|
+
MEETING_EVIDENCE_DISCLAIMER
|
|
460
|
+
]
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function recordMeetingReview(projectRoot, sourceId, input = {}) {
|
|
465
|
+
const paths = projectPaths(projectRoot);
|
|
466
|
+
assertInitialized(paths);
|
|
467
|
+
const record = readPrivateSource(paths, sourceId);
|
|
468
|
+
const source = safeSource(record);
|
|
469
|
+
if (source.freshness !== 'current') throw new Error(`Meeting source is ${source.freshness}; review requires the current source`);
|
|
470
|
+
const reviewedBy = cleanText(input.reviewedBy, 'named reviewer', 160);
|
|
471
|
+
const bundle = normaliseBundle(input.bundle, source);
|
|
472
|
+
const dispositions = normaliseDispositions(input.dispositions, bundle.candidates);
|
|
473
|
+
const reviewedAt = new Date(input.now ?? Date.now()).toISOString();
|
|
474
|
+
const payload = {
|
|
475
|
+
schema: REVIEW_SCHEMA,
|
|
476
|
+
sourceId,
|
|
477
|
+
sourceDigest: source.digest,
|
|
478
|
+
bundleDigest: canonicalDigest(bundle),
|
|
479
|
+
reviewedBy,
|
|
480
|
+
reviewedAt,
|
|
481
|
+
candidates: bundle.candidates,
|
|
482
|
+
dispositions
|
|
483
|
+
};
|
|
484
|
+
const reviewDigest = canonicalDigest(payload);
|
|
485
|
+
const review = { ...payload, reviewDigest };
|
|
486
|
+
const path = reviewRecordPath(paths, sourceId);
|
|
487
|
+
if (existsSync(path)) {
|
|
488
|
+
const existing = readReview(paths, sourceId);
|
|
489
|
+
if (existing.reviewDigest === reviewDigest) {
|
|
490
|
+
return { schema: 'ewai.meeting-review-recording/v1', sourceId, reviewDigest, reviewedBy, reviewedAt: existing.reviewedAt, idempotent: true };
|
|
491
|
+
}
|
|
492
|
+
throw new Error('Conflicting named meeting review already exists and will not be overwritten');
|
|
493
|
+
}
|
|
494
|
+
atomicJson(path, review);
|
|
495
|
+
const counts = Object.fromEntries([...reviewDecisions].map((decision) => [decision, dispositions.filter((item) => item.decision === decision).length]));
|
|
496
|
+
return { schema: 'ewai.meeting-review-recording/v1', sourceId, reviewDigest, reviewedBy, reviewedAt, counts, idempotent: false };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function promoteMeetingEvidence(projectRoot, sourceId, input = {}) {
|
|
500
|
+
if (input.confirmed !== true) throw new Error('Meeting evidence promotion requires exact confirmation');
|
|
501
|
+
const approvedBy = cleanText(input.approvedBy, 'named approver', 160);
|
|
502
|
+
const paths = projectPaths(projectRoot);
|
|
503
|
+
assertInitialized(paths);
|
|
504
|
+
cleanPromotionTransaction(paths, sourceId);
|
|
505
|
+
const privateSource = readPrivateSource(paths, sourceId);
|
|
506
|
+
const source = safeSource(privateSource);
|
|
507
|
+
if (source.freshness !== 'current') throw new Error(`Meeting source is ${source.freshness}; promotion requires the reviewed source digest`);
|
|
508
|
+
const review = readReview(paths, sourceId);
|
|
509
|
+
if (review.sourceDigest !== source.digest) throw new Error('Meeting source is stale relative to the named review');
|
|
510
|
+
const dispositions = new Map(review.dispositions.map((item) => [item.candidateId, item]));
|
|
511
|
+
const evidence = review.candidates.flatMap((candidate) => {
|
|
512
|
+
const decision = dispositions.get(candidate.id);
|
|
513
|
+
if (!['accepted', 'amended'].includes(decision.decision)) return [];
|
|
514
|
+
return [{
|
|
515
|
+
id: candidate.id,
|
|
516
|
+
type: candidate.type,
|
|
517
|
+
statement: decision.decision === 'amended' ? decision.replacementText : candidate.observedStatement,
|
|
518
|
+
interpretation: candidate.interpretation,
|
|
519
|
+
lineAnchors: candidate.lineAnchors,
|
|
520
|
+
confidence: candidate.confidence,
|
|
521
|
+
reviewDecision: decision.decision,
|
|
522
|
+
...(decision.rationale ? { rationale: decision.rationale } : {})
|
|
523
|
+
}];
|
|
524
|
+
});
|
|
525
|
+
if (!evidence.length) throw new Error('Meeting review has no accepted or amended evidence to promote');
|
|
526
|
+
const targets = evidencePaths(paths, sourceId);
|
|
527
|
+
if (existsSync(targets.json) || existsSync(targets.markdown)) {
|
|
528
|
+
if (!existsSync(targets.json) || !existsSync(targets.markdown)) throw new Error('Existing meeting evidence pair is incomplete');
|
|
529
|
+
const existing = JSON.parse(readFileSync(targets.json, 'utf8'));
|
|
530
|
+
const samePromotion = existing.source?.digest === source.digest
|
|
531
|
+
&& existing.review?.digest === review.reviewDigest
|
|
532
|
+
&& existing.promotion?.approvedBy === approvedBy
|
|
533
|
+
&& JSON.stringify(existing.evidence) === JSON.stringify(evidence)
|
|
534
|
+
&& sha256(readFileSync(targets.markdown, 'utf8')) === existing.markdownDigest;
|
|
535
|
+
if (samePromotion) {
|
|
536
|
+
return {
|
|
537
|
+
schema: 'ewai.meeting-promotion-result/v1', sourceId, evidenceDigest: existing.evidenceDigest,
|
|
538
|
+
jsonPath: safeRelative(paths.projectRoot, targets.json), markdownPath: safeRelative(paths.projectRoot, targets.markdown),
|
|
539
|
+
counts: { promoted: evidence.length, rejected: review.dispositions.filter(({ decision }) => decision === 'rejected').length, deferred: review.dispositions.filter(({ decision }) => decision === 'deferred').length },
|
|
540
|
+
lifecycle: { status: 'already-published' }, idempotent: true
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
throw new Error('Conflicting meeting evidence already exists and will not be overwritten');
|
|
544
|
+
}
|
|
545
|
+
const approvedAt = new Date(input.now ?? Date.now()).toISOString();
|
|
546
|
+
const payload = {
|
|
547
|
+
schema: EVIDENCE_SCHEMA,
|
|
548
|
+
source: {
|
|
549
|
+
sourceId: source.sourceId,
|
|
550
|
+
label: source.label,
|
|
551
|
+
classification: source.classification,
|
|
552
|
+
digest: source.digest,
|
|
553
|
+
lineCount: source.lineCount
|
|
554
|
+
},
|
|
555
|
+
review: { digest: review.reviewDigest, reviewedBy: review.reviewedBy, reviewedAt: review.reviewedAt },
|
|
556
|
+
promotion: { approvedBy, approvedAt },
|
|
557
|
+
evidence
|
|
558
|
+
};
|
|
559
|
+
const evidenceDigest = canonicalDigest(payload);
|
|
560
|
+
const record = { ...payload, evidenceDigest };
|
|
561
|
+
const markdown = renderEvidence(record);
|
|
562
|
+
record.markdownDigest = sha256(markdown);
|
|
563
|
+
const json = `${JSON.stringify(record, null, 2)}\n`;
|
|
564
|
+
const transaction = transactionPaths(paths, sourceId);
|
|
565
|
+
mkdirSync(targets.root, { recursive: true });
|
|
566
|
+
mkdirSync(transaction.root, { recursive: true });
|
|
567
|
+
const nonce = `${process.pid}-${randomUUID()}`;
|
|
568
|
+
const jsonStage = `${targets.json}.${nonce}.tmp`;
|
|
569
|
+
const markdownStage = `${targets.markdown}.${nonce}.tmp`;
|
|
570
|
+
const journal = { schema: 'ewai.meeting-evidence-transaction/v1', sourceId, phase: 'staged', jsonStage, markdownStage };
|
|
571
|
+
try {
|
|
572
|
+
writeFileSync(jsonStage, json, 'utf8');
|
|
573
|
+
writeFileSync(markdownStage, markdown, 'utf8');
|
|
574
|
+
atomicJson(transaction.journal, journal);
|
|
575
|
+
renameSync(jsonStage, targets.json);
|
|
576
|
+
journal.phase = 'json-committed';
|
|
577
|
+
atomicJson(transaction.journal, journal);
|
|
578
|
+
if (input.testHooks?.failAfterStage === 'json-committed') throw new Error('Meeting evidence write was interrupted after JSON commit');
|
|
579
|
+
renameSync(markdownStage, targets.markdown);
|
|
580
|
+
journal.phase = 'committed';
|
|
581
|
+
atomicJson(transaction.journal, journal);
|
|
582
|
+
rmSync(transaction.journal, { force: true });
|
|
583
|
+
} catch (error) {
|
|
584
|
+
rmSync(jsonStage, { force: true });
|
|
585
|
+
rmSync(markdownStage, { force: true });
|
|
586
|
+
rmSync(targets.json, { force: true });
|
|
587
|
+
rmSync(targets.markdown, { force: true });
|
|
588
|
+
rmSync(transaction.journal, { force: true });
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const jsonPath = safeRelative(paths.projectRoot, targets.json);
|
|
593
|
+
const markdownPath = safeRelative(paths.projectRoot, targets.markdown);
|
|
594
|
+
const publication = publishLifecycleEventSafely(paths.projectRoot, 'ewai.meeting-evidence.promoted', {
|
|
595
|
+
sourceKey: `meeting-evidence:${sourceId}:${evidenceDigest}`,
|
|
596
|
+
sourceRevision: evidenceDigest,
|
|
597
|
+
occurredAt: approvedAt,
|
|
598
|
+
streamId: `meeting-evidence:${sourceId}`,
|
|
599
|
+
facts: { sourceId, evidenceDigest, candidateCount: evidence.length, approvedAt },
|
|
600
|
+
evidence: [jsonPath, markdownPath]
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
schema: 'ewai.meeting-promotion-result/v1', sourceId, evidenceDigest, jsonPath, markdownPath,
|
|
604
|
+
counts: { promoted: evidence.length, rejected: review.dispositions.filter(({ decision }) => decision === 'rejected').length, deferred: review.dispositions.filter(({ decision }) => decision === 'deferred').length },
|
|
605
|
+
lifecycle: { ...publication, status: publication.event ? 'published' : publication.status ?? 'publication-failed' },
|
|
606
|
+
idempotent: false
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function readMeetingEvidenceWorkspace(projectRoot, options = {}) {
|
|
611
|
+
const paths = projectPaths(projectRoot);
|
|
612
|
+
assertInitialized(paths);
|
|
613
|
+
const root = sourceRoot(paths);
|
|
614
|
+
const sources = existsSync(root)
|
|
615
|
+
? readdirSync(root, { withFileTypes: true })
|
|
616
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
617
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
618
|
+
.map((entry) => {
|
|
619
|
+
const record = JSON.parse(readFileSync(resolve(root, entry.name), 'utf8'));
|
|
620
|
+
if (record.schema !== SOURCE_SCHEMA) return null;
|
|
621
|
+
const source = safeSource(record);
|
|
622
|
+
const reviewPath = reviewRecordPath(paths, source.sourceId);
|
|
623
|
+
const targets = evidencePaths(paths, source.sourceId);
|
|
624
|
+
const privateReview = existsSync(reviewPath) ? readReview(paths, source.sourceId) : null;
|
|
625
|
+
const promoted = existsSync(targets.json) && existsSync(targets.markdown)
|
|
626
|
+
? JSON.parse(readFileSync(targets.json, 'utf8'))
|
|
627
|
+
: null;
|
|
628
|
+
return {
|
|
629
|
+
...source,
|
|
630
|
+
reviewState: privateReview ? 'reviewed' : 'pending',
|
|
631
|
+
promotionState: promoted ? 'promoted' : 'not-promoted',
|
|
632
|
+
candidateCount: privateReview?.candidates?.length ?? 0,
|
|
633
|
+
reviewedCount: privateReview?.dispositions?.length ?? 0,
|
|
634
|
+
promotedCount: promoted?.evidence?.length ?? 0
|
|
635
|
+
};
|
|
636
|
+
})
|
|
637
|
+
.filter(Boolean)
|
|
638
|
+
: [];
|
|
639
|
+
const selected = options.sourceId
|
|
640
|
+
? sources.find((source) => source.sourceId === options.sourceId) ?? null
|
|
641
|
+
: sources[0] ?? null;
|
|
642
|
+
if (options.sourceId && !selected) throw new Error(`Unknown meeting source: ${options.sourceId}`);
|
|
643
|
+
const activePersonas = selected ? contextualPersonas(paths.projectRoot, selected, options) : [];
|
|
644
|
+
let review = null;
|
|
645
|
+
let candidates = [];
|
|
646
|
+
let promotion = null;
|
|
647
|
+
if (selected?.reviewState === 'reviewed') {
|
|
648
|
+
const privateReview = readReview(paths, selected.sourceId);
|
|
649
|
+
const dispositions = new Map(privateReview.dispositions.map((item) => [item.candidateId, item]));
|
|
650
|
+
review = {
|
|
651
|
+
digest: privateReview.reviewDigest,
|
|
652
|
+
reviewedBy: privateReview.reviewedBy,
|
|
653
|
+
reviewedAt: privateReview.reviewedAt,
|
|
654
|
+
sourceDigest: privateReview.sourceDigest,
|
|
655
|
+
fresh: privateReview.sourceDigest === selected.digest && selected.freshness === 'current'
|
|
656
|
+
};
|
|
657
|
+
candidates = privateReview.candidates.map((candidate) => ({
|
|
658
|
+
...candidate,
|
|
659
|
+
disposition: dispositions.get(candidate.id)
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
if (selected?.promotionState === 'promoted') {
|
|
663
|
+
const targets = evidencePaths(paths, selected.sourceId);
|
|
664
|
+
const recorded = JSON.parse(readFileSync(targets.json, 'utf8'));
|
|
665
|
+
promotion = {
|
|
666
|
+
evidenceDigest: recorded.evidenceDigest,
|
|
667
|
+
approvedBy: recorded.promotion?.approvedBy,
|
|
668
|
+
approvedAt: recorded.promotion?.approvedAt,
|
|
669
|
+
promotedCount: Array.isArray(recorded.evidence) ? recorded.evidence.length : 0,
|
|
670
|
+
evidence: [safeRelative(paths.projectRoot, targets.json), safeRelative(paths.projectRoot, targets.markdown)]
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
schema: WORKSPACE_SCHEMA,
|
|
675
|
+
sources,
|
|
676
|
+
selectedSourceId: selected?.sourceId ?? null,
|
|
677
|
+
selectedSource: selected,
|
|
678
|
+
review,
|
|
679
|
+
candidates,
|
|
680
|
+
promotion,
|
|
681
|
+
activePersonas,
|
|
682
|
+
counts: {
|
|
683
|
+
sources: sources.length,
|
|
684
|
+
current: sources.filter((source) => source.freshness === 'current').length,
|
|
685
|
+
stale: sources.filter((source) => source.freshness !== 'current').length
|
|
686
|
+
},
|
|
687
|
+
candidateCounts: Object.fromEntries([...reviewDecisions].map((decision) => [
|
|
688
|
+
decision,
|
|
689
|
+
candidates.filter((candidate) => candidate.disposition?.decision === decision).length
|
|
690
|
+
])),
|
|
691
|
+
actions: {
|
|
692
|
+
register: true,
|
|
693
|
+
prepare: Boolean(selected && selected.freshness === 'current' && selected.cloudProcessing === 'allowed'),
|
|
694
|
+
review: Boolean(selected && selected.freshness === 'current'),
|
|
695
|
+
promote: Boolean(selected && selected.freshness === 'current' && selected.reviewState === 'reviewed' && selected.promotionState !== 'promoted')
|
|
696
|
+
},
|
|
697
|
+
notices: [
|
|
698
|
+
'Raw meeting text and absolute source paths are intentionally absent from this workspace.',
|
|
699
|
+
'Persona engagement is contextual and advisory. A named human owns review and promotion.',
|
|
700
|
+
MEETING_EVIDENCE_DISCLAIMER
|
|
701
|
+
]
|
|
702
|
+
};
|
|
703
|
+
}
|