@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,457 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { projectPaths } from '../paths.mjs';
|
|
5
|
+
import { selectContextualPersonas } from './persona-engagement.mjs';
|
|
6
|
+
|
|
7
|
+
export const CONTEXT_ESTIMATE_METHOD = 'utf8-bytes-div-3-v1';
|
|
8
|
+
|
|
9
|
+
const profile = (id, label, signals, defaultBudgetTokens) => Object.freeze({
|
|
10
|
+
id, label, signals: Object.freeze(signals), defaultBudgetTokens,
|
|
11
|
+
minBudgetTokens: 32, maxBudgetTokens: 200_000,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const contextProfiles = Object.freeze({
|
|
15
|
+
companion: profile('companion', 'Companion guidance', ['current work', 'delivery', 'human route', 'recovery'], 2_000),
|
|
16
|
+
intent: profile('intent', 'Intent evidence', ['outcomes', 'acceptance', 'users', 'constraints'], 4_000),
|
|
17
|
+
plan: profile('plan', 'Delivery plan', ['architecture', 'standards', 'repository', 'testing'], 8_000),
|
|
18
|
+
'build-task': profile('build-task', 'Build task', ['build', 'implementation', 'testing'], 12_000),
|
|
19
|
+
'fresh-context-review': profile('fresh-context-review', 'Fresh-context review', ['review', 'security', 'testing', 'standards'], 12_000),
|
|
20
|
+
'phase-contribution-review': profile('phase-contribution-review', 'Phase contribution review', ['evidence', 'review', 'attribution', 'outcomes'], 6_000),
|
|
21
|
+
'design-system-apply': profile('design-system-apply', 'Design-system application', ['design', 'prototype', 'interaction', 'content', 'states', 'accessibility'], 6_000),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const evidenceOrder = Object.freeze({ mandatory: 0, relevant: 1, optional: 2 });
|
|
25
|
+
|
|
26
|
+
function clean(value) {
|
|
27
|
+
return String(value ?? '').trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function digest(value) {
|
|
31
|
+
return createHash('sha256').update(value).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function canonical(value) {
|
|
35
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
36
|
+
if (value && typeof value === 'object') {
|
|
37
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function unique(values) {
|
|
43
|
+
return [...new Set((values ?? []).map(clean).filter(Boolean))];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function safeRelativePath(value) {
|
|
47
|
+
const path = clean(value).replaceAll('\\', '/');
|
|
48
|
+
if (!path || isAbsolute(path) || path.startsWith('/') || path.split('/').includes('..')) {
|
|
49
|
+
throw new Error(`Context sourcePath must be project-relative: ${value}`);
|
|
50
|
+
}
|
|
51
|
+
return path.replace(/^\.\//, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function estimateContextTokens(value) {
|
|
55
|
+
const bytes = Buffer.byteLength(String(value ?? ''), 'utf8');
|
|
56
|
+
return { bytes, estimatedTokens: Math.ceil(bytes / 3), method: CONTEXT_ESTIMATE_METHOD };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normaliseCandidate(candidate, policyDigest) {
|
|
60
|
+
const id = clean(candidate?.id);
|
|
61
|
+
const label = clean(candidate?.label);
|
|
62
|
+
const evidenceClass = clean(candidate?.evidenceClass).toLowerCase();
|
|
63
|
+
const content = String(candidate?.content ?? '');
|
|
64
|
+
if (!id || !/^[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?$/i.test(id)) throw new Error(`Invalid context segment id: ${id || '<empty>'}`);
|
|
65
|
+
if (!label) throw new Error(`Context segment ${id} requires a label.`);
|
|
66
|
+
if (!(evidenceClass in evidenceOrder)) throw new Error(`Context segment ${id} has unsupported evidence class: ${evidenceClass}`);
|
|
67
|
+
if (!content) throw new Error(`Context segment ${id} has no content.`);
|
|
68
|
+
const sourcePath = safeRelativePath(candidate.sourcePath);
|
|
69
|
+
const sourceDigest = clean(candidate.sourceDigest) || digest(content);
|
|
70
|
+
const requiredMarkers = unique(candidate.requiredMarkers);
|
|
71
|
+
const fragmentDigest = digest(canonical({ sourceDigest, policyDigest, content }));
|
|
72
|
+
const normalised = {
|
|
73
|
+
id,
|
|
74
|
+
label,
|
|
75
|
+
evidenceClass,
|
|
76
|
+
priority: Number.isFinite(Number(candidate.priority)) ? Number(candidate.priority) : 0,
|
|
77
|
+
sourcePath,
|
|
78
|
+
sourceDigest,
|
|
79
|
+
policyDigest,
|
|
80
|
+
fragmentDigest,
|
|
81
|
+
freshness: clean(candidate.freshness) || 'current',
|
|
82
|
+
selectionReason: clean(candidate.selectionReason) || `${label} is ${evidenceClass} for this profile.`,
|
|
83
|
+
minimumRepresentation: clean(candidate.minimumRepresentation) || (requiredMarkers.length ? `required-marker-count:${requiredMarkers.length}` : 'complete-segment'),
|
|
84
|
+
requiredMarkers,
|
|
85
|
+
content,
|
|
86
|
+
};
|
|
87
|
+
return { ...normalised, ...estimateContextTokens(renderedSegment(normalised)) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function compareSegments(left, right) {
|
|
91
|
+
return evidenceOrder[left.evidenceClass] - evidenceOrder[right.evidenceClass]
|
|
92
|
+
|| right.priority - left.priority
|
|
93
|
+
|| left.id.localeCompare(right.id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cacheFragment(cacheRoot, segment) {
|
|
97
|
+
if (!cacheRoot) return { status: 'disabled' };
|
|
98
|
+
const root = resolve(cacheRoot);
|
|
99
|
+
const path = resolve(root, 'fragments', `${segment.fragmentDigest}.txt`);
|
|
100
|
+
mkdirSync(resolve(root, 'fragments'), { recursive: true });
|
|
101
|
+
if (existsSync(path) && readFileSync(path, 'utf8') === segment.content) return { status: 'reused' };
|
|
102
|
+
writeFileSync(path, segment.content, { encoding: 'utf8', mode: 0o600 });
|
|
103
|
+
return { status: 'written' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function previousManifest(options) {
|
|
107
|
+
if (!options.previousPack && !options.previousDigest) return null;
|
|
108
|
+
if (!options.previousPack || clean(options.previousPack.digest) !== clean(options.previousDigest)) {
|
|
109
|
+
throw new Error('The exact predecessor digest does not match the supplied previous pack.');
|
|
110
|
+
}
|
|
111
|
+
return options.previousPack;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function segmentDelta(current, previous) {
|
|
115
|
+
if (!previous) return { predecessorDigest: null, addedSegments: current.map(({ id }) => id), changedSegments: [], retainedSegments: [], removedSegments: [] };
|
|
116
|
+
const before = new Map((previous.segments ?? []).map((segment) => [segment.id, segment]));
|
|
117
|
+
const after = new Map(current.map((segment) => [segment.id, segment]));
|
|
118
|
+
return {
|
|
119
|
+
predecessorDigest: previous.digest,
|
|
120
|
+
addedSegments: current.filter(({ id }) => !before.has(id)).map(({ id }) => id),
|
|
121
|
+
changedSegments: current.filter(({ id, fragmentDigest }) => before.has(id) && before.get(id).fragmentDigest !== fragmentDigest).map(({ id }) => id),
|
|
122
|
+
retainedSegments: current.filter(({ id, fragmentDigest }) => before.get(id)?.fragmentDigest === fragmentDigest).map(({ id }) => id),
|
|
123
|
+
removedSegments: [...before.keys()].filter((id) => !after.has(id)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function personaDelta(current, previous) {
|
|
128
|
+
const before = new Set((previous?.activePersonas ?? []).map(({ id }) => id));
|
|
129
|
+
const after = new Set(current.map(({ id }) => id));
|
|
130
|
+
return {
|
|
131
|
+
addedPersonas: [...after].filter((id) => !before.has(id)),
|
|
132
|
+
retainedPersonas: [...after].filter((id) => before.has(id)),
|
|
133
|
+
removedPersonas: [...before].filter((id) => !after.has(id)),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function compareContextPacks(currentPack, predecessorPack = null, predecessorDigest = '') {
|
|
138
|
+
const previous = previousManifest({ previousPack: predecessorPack, previousDigest: predecessorDigest });
|
|
139
|
+
const currentSegments = (currentPack?.segments ?? []).filter(({ disposition }) => ['selected', 'reused'].includes(disposition));
|
|
140
|
+
return {
|
|
141
|
+
...segmentDelta(currentSegments, previous),
|
|
142
|
+
...personaDelta(currentPack?.activePersonas ?? [], previous),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderedSegment(segment) {
|
|
147
|
+
return `## ${segment.label} [${segment.id}]\nSource: ${segment.sourcePath}\nEvidence: ${segment.evidenceClass}\n\n${segment.content}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function safeSegment(segment) {
|
|
151
|
+
const { content: _content, requiredMarkers: _requiredMarkers, ...safe } = segment;
|
|
152
|
+
return safe;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function contextPackDigest(pack) {
|
|
156
|
+
const safe = structuredClone(pack);
|
|
157
|
+
delete safe.digest;
|
|
158
|
+
delete safe.modelContext;
|
|
159
|
+
delete safe.deltaContext;
|
|
160
|
+
safe.segments = (safe.segments ?? []).map((segment) => {
|
|
161
|
+
const { cache: _cache, ...stable } = segment;
|
|
162
|
+
return stable;
|
|
163
|
+
});
|
|
164
|
+
return digest(canonical(safe));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function prepareContextPack(options = {}) {
|
|
168
|
+
const selectedProfile = contextProfiles[clean(options.profile)];
|
|
169
|
+
if (!selectedProfile) throw new Error(`Unsupported context profile: ${clean(options.profile) || '<empty>'}`);
|
|
170
|
+
const predecessor = previousManifest(options);
|
|
171
|
+
const budgetTokens = Number(options.budgetTokens ?? selectedProfile.defaultBudgetTokens);
|
|
172
|
+
if (!Number.isInteger(budgetTokens) || budgetTokens < 1 || budgetTokens > selectedProfile.maxBudgetTokens) {
|
|
173
|
+
throw new Error(`Context budget must be an integer from 1 to ${selectedProfile.maxBudgetTokens}.`);
|
|
174
|
+
}
|
|
175
|
+
const policyDigest = digest(canonical({
|
|
176
|
+
profile: selectedProfile,
|
|
177
|
+
policyVersion: 1,
|
|
178
|
+
mandatory: (options.candidates ?? []).filter((candidate) => candidate.evidenceClass === 'mandatory')
|
|
179
|
+
.map(({ id, requiredMarkers, minimumRepresentation }) => ({ id, requiredMarkers: unique(requiredMarkers), minimumRepresentation: clean(minimumRepresentation) }))
|
|
180
|
+
.sort((left, right) => clean(left.id).localeCompare(clean(right.id))),
|
|
181
|
+
}));
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
const candidates = (options.candidates ?? []).map((candidate) => normaliseCandidate(candidate, policyDigest)).sort(compareSegments);
|
|
184
|
+
for (const candidate of candidates) {
|
|
185
|
+
if (seen.has(candidate.id)) throw new Error(`Duplicate context segment id: ${candidate.id}`);
|
|
186
|
+
seen.add(candidate.id);
|
|
187
|
+
}
|
|
188
|
+
if (!candidates.some(({ evidenceClass }) => evidenceClass === 'mandatory')) {
|
|
189
|
+
throw new Error(`Context profile ${selectedProfile.id} has no mandatory evidence candidates.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const mandatoryCandidates = candidates.filter(({ evidenceClass }) => evidenceClass === 'mandatory');
|
|
193
|
+
const mandatoryDemand = estimateContextTokens(mandatoryCandidates.map(renderedSegment).join('\n\n')).estimatedTokens;
|
|
194
|
+
const overflow = mandatoryDemand > budgetTokens;
|
|
195
|
+
let usedTokens = overflow ? mandatoryDemand : 0;
|
|
196
|
+
const selectedIds = new Set();
|
|
197
|
+
const selectedRendered = [];
|
|
198
|
+
if (!overflow) {
|
|
199
|
+
for (const candidate of mandatoryCandidates) {
|
|
200
|
+
selectedIds.add(candidate.id);
|
|
201
|
+
selectedRendered.push(renderedSegment(candidate));
|
|
202
|
+
}
|
|
203
|
+
usedTokens = estimateContextTokens(selectedRendered.join('\n\n')).estimatedTokens;
|
|
204
|
+
for (const candidate of candidates.filter(({ evidenceClass }) => evidenceClass !== 'mandatory')) {
|
|
205
|
+
const candidateRendered = renderedSegment(candidate);
|
|
206
|
+
const nextTokens = estimateContextTokens([...selectedRendered, candidateRendered].join('\n\n')).estimatedTokens;
|
|
207
|
+
if (nextTokens > budgetTokens) continue;
|
|
208
|
+
selectedIds.add(candidate.id);
|
|
209
|
+
selectedRendered.push(candidateRendered);
|
|
210
|
+
usedTokens = nextTokens;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const priorSegments = new Map((predecessor?.segments ?? []).map((segment) => [segment.id, segment]));
|
|
214
|
+
const segments = candidates.map((candidate) => {
|
|
215
|
+
let disposition = 'deferred';
|
|
216
|
+
if (overflow) disposition = candidate.evidenceClass === 'mandatory' ? 'overflow' : 'deferred';
|
|
217
|
+
else if (selectedIds.has(candidate.id)) {
|
|
218
|
+
disposition = priorSegments.get(candidate.id)?.fragmentDigest === candidate.fragmentDigest ? 'reused' : 'selected';
|
|
219
|
+
}
|
|
220
|
+
const cache = ['selected', 'reused'].includes(disposition) ? cacheFragment(options.cacheRoot, candidate) : { status: 'not-selected' };
|
|
221
|
+
return { ...candidate, disposition, cache };
|
|
222
|
+
});
|
|
223
|
+
const included = segments.filter(({ disposition }) => ['selected', 'reused'].includes(disposition));
|
|
224
|
+
const activePersonas = selectContextualPersonas({
|
|
225
|
+
personaCatalogue: options.personaCatalogue,
|
|
226
|
+
signals: unique([...selectedProfile.signals, ...clean(options.focus).split(/[^a-z0-9]+/i)]),
|
|
227
|
+
context: { profile: selectedProfile.id, focus: clean(options.focus), selected: included.map(({ id }) => id) },
|
|
228
|
+
contextLabel: selectedProfile.label,
|
|
229
|
+
limit: options.personaLimit ?? 4,
|
|
230
|
+
});
|
|
231
|
+
const required = segments.filter(({ evidenceClass }) => evidenceClass === 'mandatory')
|
|
232
|
+
.flatMap((segment) => segment.requiredMarkers.map((marker) => ({ segmentId: segment.id, marker, present: included.some(({ id, content }) => id === segment.id && content.includes(marker)) })));
|
|
233
|
+
const presentCount = required.filter(({ present }) => present).length;
|
|
234
|
+
const mandatoryRecall = required.length ? presentCount / required.length : 1;
|
|
235
|
+
const fidelity = {
|
|
236
|
+
status: !overflow && mandatoryRecall === 1 ? 'pass' : 'fail',
|
|
237
|
+
mandatoryRecall,
|
|
238
|
+
requiredCount: required.length,
|
|
239
|
+
presentCount,
|
|
240
|
+
missing: required.filter(({ present }) => !present).map(({ segmentId, marker }) => `${segmentId}:${marker}`),
|
|
241
|
+
checks: ['mandatory-evidence', 'standards', 'task-boundaries', 'tests', 'security-privacy', 'contradictions', 'human-authority'],
|
|
242
|
+
};
|
|
243
|
+
const modelContext = overflow ? null : included.map(renderedSegment).join('\n\n');
|
|
244
|
+
const delta = compareContextPacks({ segments: included, activePersonas }, predecessor, predecessor?.digest);
|
|
245
|
+
const deltaIds = new Set([...delta.addedSegments, ...delta.changedSegments]);
|
|
246
|
+
const deltaContext = overflow || !predecessor ? modelContext : included.filter(({ id }) => deltaIds.has(id)).map(renderedSegment).join('\n\n');
|
|
247
|
+
const core = {
|
|
248
|
+
schema: 'ewai.context-pack/v1',
|
|
249
|
+
status: overflow ? 'non-ready' : 'ready',
|
|
250
|
+
reason: overflow ? 'mandatory-overflow' : 'within-budget',
|
|
251
|
+
profile: selectedProfile.id,
|
|
252
|
+
profileLabel: selectedProfile.label,
|
|
253
|
+
focus: clean(options.focus),
|
|
254
|
+
revisions: { repository: clean(options.repositoryRevision) || 'workspace', delivery: clean(options.deliveryRevision) || 'unversioned' },
|
|
255
|
+
policyDigest,
|
|
256
|
+
budget: {
|
|
257
|
+
limitTokens: budgetTokens,
|
|
258
|
+
usedTokens,
|
|
259
|
+
mandatoryTokens: mandatoryDemand,
|
|
260
|
+
overflowTokens: Math.max(0, mandatoryDemand - budgetTokens),
|
|
261
|
+
estimateMethod: CONTEXT_ESTIMATE_METHOD,
|
|
262
|
+
},
|
|
263
|
+
segments: segments.map(safeSegment),
|
|
264
|
+
activePersonas,
|
|
265
|
+
delta,
|
|
266
|
+
fidelity,
|
|
267
|
+
usage: {
|
|
268
|
+
estimatedInputBytes: modelContext === null ? 0 : Buffer.byteLength(modelContext, 'utf8'),
|
|
269
|
+
estimatedInputTokens: modelContext === null ? 0 : estimateContextTokens(modelContext).estimatedTokens,
|
|
270
|
+
estimateMethod: CONTEXT_ESTIMATE_METHOD,
|
|
271
|
+
providerReported: null,
|
|
272
|
+
},
|
|
273
|
+
authority: {
|
|
274
|
+
level: 'none',
|
|
275
|
+
notices: ['Context preparation cannot approve Build or Manual QA.', 'Engineering fidelity evidence is not certification or release authority.'],
|
|
276
|
+
},
|
|
277
|
+
recovery: overflow ? ['narrow-focus', 'increase-budget', 'split-operation'] : [],
|
|
278
|
+
};
|
|
279
|
+
const packDigest = contextPackDigest(core);
|
|
280
|
+
return { ...core, digest: packDigest, modelContext, deltaContext };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function safeContextManifest(pack) {
|
|
284
|
+
const { modelContext: _modelContext, deltaContext: _deltaContext, ...safe } = structuredClone(pack);
|
|
285
|
+
safe.segments = (safe.segments ?? []).map(safeSegment);
|
|
286
|
+
return safe;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function contextRuntimeRoot(projectRoot) {
|
|
290
|
+
return resolve(projectPaths(projectRoot).runtimeRoot, 'runtime/context-packs');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function findIntentPath(root, slug) {
|
|
294
|
+
if (!existsSync(root)) return '';
|
|
295
|
+
for (const entry of readdirSync(root, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
296
|
+
const path = resolve(root, entry.name);
|
|
297
|
+
if (entry.isDirectory()) {
|
|
298
|
+
const found = findIntentPath(path, slug);
|
|
299
|
+
if (found) return found;
|
|
300
|
+
} else if (entry.isFile() && entry.name === `${slug}.md`) return path;
|
|
301
|
+
}
|
|
302
|
+
return '';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function sourceCandidate(projectRoot, absolutePath, options = {}) {
|
|
306
|
+
if (!existsSync(absolutePath)) {
|
|
307
|
+
if (options.required) throw new Error(`Required context source is missing: ${options.label}.`);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const content = readFileSync(absolutePath, 'utf8');
|
|
311
|
+
return {
|
|
312
|
+
id: options.id,
|
|
313
|
+
label: options.label,
|
|
314
|
+
evidenceClass: options.evidenceClass ?? 'relevant',
|
|
315
|
+
priority: options.priority ?? 50,
|
|
316
|
+
sourcePath: relative(resolve(projectRoot), absolutePath).replaceAll('\\', '/'),
|
|
317
|
+
content,
|
|
318
|
+
requiredMarkers: options.requiredMarkers ?? [],
|
|
319
|
+
selectionReason: options.selectionReason,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function contextAuthorityCandidate() {
|
|
324
|
+
return {
|
|
325
|
+
id: 'authority-boundary',
|
|
326
|
+
label: 'Human authority boundary',
|
|
327
|
+
evidenceClass: 'mandatory',
|
|
328
|
+
priority: 1_000,
|
|
329
|
+
sourcePath: 'Docs/human-approval-and-assurance-guide.md',
|
|
330
|
+
content: 'AUTHORITY_NONE Context preparation cannot approve Build or Manual QA, accept security risk, certify quality, deploy or release.',
|
|
331
|
+
requiredMarkers: ['AUTHORITY_NONE', 'cannot approve Build or Manual QA', 'deploy or release'],
|
|
332
|
+
selectionReason: 'Every profile retains explicit human approval and release boundaries.',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function profileCandidates(projectRoot, input) {
|
|
337
|
+
const project = projectPaths(projectRoot);
|
|
338
|
+
const slug = clean(input.slug);
|
|
339
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) throw new Error('Context preparation requires a lower kebab-case --slug.');
|
|
340
|
+
const buildRoot = resolve(project.buildRoot, slug);
|
|
341
|
+
const authority = contextAuthorityCandidate();
|
|
342
|
+
if (input.profile === 'intent') {
|
|
343
|
+
const intentPath = findIntentPath(resolve(project.specsRoot, '2.Purpose/intents'), slug);
|
|
344
|
+
if (!intentPath) throw new Error(`Intent not found for context preparation: ${slug}.`);
|
|
345
|
+
const content = readFileSync(intentPath, 'utf8');
|
|
346
|
+
const title = content.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? slug;
|
|
347
|
+
return [authority, sourceCandidate(projectRoot, intentPath, {
|
|
348
|
+
id: 'intent-source', label: 'Canonical intent', evidenceClass: 'mandatory', priority: 900,
|
|
349
|
+
required: true, requiredMarkers: [slug, title], selectionReason: 'Intent purpose, outcomes and acceptance are mandatory.',
|
|
350
|
+
})];
|
|
351
|
+
}
|
|
352
|
+
if (input.profile === 'companion') {
|
|
353
|
+
return [authority,
|
|
354
|
+
sourceCandidate(projectRoot, resolve(buildRoot, 'delivery-state.json'), {
|
|
355
|
+
id: 'delivery-state', label: 'Current delivery state', evidenceClass: 'mandatory', priority: 900,
|
|
356
|
+
required: true, requiredMarkers: [slug, 'currentPhase'], selectionReason: 'Companion guidance requires current governed state.',
|
|
357
|
+
}),
|
|
358
|
+
sourceCandidate(projectRoot, resolve(buildRoot, 'tracker.md'), {
|
|
359
|
+
id: 'delivery-tracker', label: 'Delivery tracker', evidenceClass: 'relevant', priority: 70,
|
|
360
|
+
selectionReason: 'The tracker explains recent phase progress.',
|
|
361
|
+
}),
|
|
362
|
+
sourceCandidate(projectRoot, resolve(buildRoot, 'context-packet.md'), {
|
|
363
|
+
id: 'context-packet', label: 'Delivery context packet', evidenceClass: 'optional', priority: 20,
|
|
364
|
+
selectionReason: 'The complete packet is included only when the profile budget permits.',
|
|
365
|
+
})].filter(Boolean);
|
|
366
|
+
}
|
|
367
|
+
if (input.profile === 'plan') {
|
|
368
|
+
return [authority,
|
|
369
|
+
sourceCandidate(projectRoot, resolve(buildRoot, 'build-plan.md'), {
|
|
370
|
+
id: 'build-plan', label: 'Governed Build Plan', evidenceClass: 'mandatory', priority: 900,
|
|
371
|
+
required: true, requiredMarkers: [slug, 'Security', 'Tests'], selectionReason: 'Plan slices, interfaces and safety decisions are mandatory.',
|
|
372
|
+
}),
|
|
373
|
+
sourceCandidate(projectRoot, resolve(buildRoot, 'gates/plan/claim-ledger.json'), {
|
|
374
|
+
id: 'claim-ledger', label: 'Claim Ledger', evidenceClass: 'mandatory', priority: 850,
|
|
375
|
+
required: true, requiredMarkers: ['implementation_claims', 'tests_required'], selectionReason: 'Implementation claims and test obligations are mandatory.',
|
|
376
|
+
}),
|
|
377
|
+
sourceCandidate(projectRoot, resolve(buildRoot, 'task-graph.json'), {
|
|
378
|
+
id: 'task-graph', label: 'Task graph', evidenceClass: 'relevant', priority: 70,
|
|
379
|
+
selectionReason: 'Task topology is relevant when implementation decomposition is in focus.',
|
|
380
|
+
})].filter(Boolean);
|
|
381
|
+
}
|
|
382
|
+
if (['build-task', 'fresh-context-review'].includes(input.profile)) {
|
|
383
|
+
const graphPath = resolve(buildRoot, 'task-graph.json');
|
|
384
|
+
if (!existsSync(graphPath)) throw new Error(`Task graph not found for context preparation: ${slug}.`);
|
|
385
|
+
const graph = JSON.parse(readFileSync(graphPath, 'utf8'));
|
|
386
|
+
const taskId = clean(input.taskId);
|
|
387
|
+
const task = (graph.tasks ?? []).find(({ id }) => id === taskId);
|
|
388
|
+
if (!task) throw new Error(`Context preparation requires a valid --task for ${input.profile}.`);
|
|
389
|
+
const taskContent = JSON.stringify(task);
|
|
390
|
+
const markers = [task.id, task.slice, ...(task.claims ?? []), ...(task.write_set ?? []), ...(task.allowed_commands ?? []), ...(task.stop_conditions ?? []), task.first_failing_test].filter(Boolean);
|
|
391
|
+
const reviewRules = input.profile === 'fresh-context-review'
|
|
392
|
+
? 'TESTS_FIRST Review the diff, standards, tests, write_set, stop conditions and security. End with VERDICT: PASS or VERDICT: FAIL.'
|
|
393
|
+
: 'AUTHORITY_NONE Work only inside write_set, run allowed_commands and stop on every stop_condition.';
|
|
394
|
+
return [authority,
|
|
395
|
+
{
|
|
396
|
+
id: 'task-contract', label: 'Exact task contract', evidenceClass: 'mandatory', priority: 950,
|
|
397
|
+
sourcePath: relative(resolve(projectRoot), graphPath).replaceAll('\\', '/'), content: taskContent,
|
|
398
|
+
requiredMarkers: markers, selectionReason: 'The exact task boundary is mandatory.',
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'host-rules', label: input.profile === 'fresh-context-review' ? 'Fresh review rules' : 'Build worker rules',
|
|
402
|
+
evidenceClass: 'mandatory', priority: 900, sourcePath: 'SPECS/4.Constraints/standards/lifecycle-hook-safety.md',
|
|
403
|
+
content: reviewRules,
|
|
404
|
+
requiredMarkers: input.profile === 'fresh-context-review' ? ['TESTS_FIRST', 'VERDICT: PASS or VERDICT: FAIL'] : ['AUTHORITY_NONE', 'write_set', 'allowed_commands', 'stop_condition'],
|
|
405
|
+
selectionReason: 'Host behaviour and authority are mandatory.',
|
|
406
|
+
}];
|
|
407
|
+
}
|
|
408
|
+
if (input.profile === 'phase-contribution-review') {
|
|
409
|
+
return [authority,
|
|
410
|
+
sourceCandidate(projectRoot, resolve(buildRoot, 'delivery-state.json'), {
|
|
411
|
+
id: 'delivery-state', label: 'Current delivery state', evidenceClass: 'mandatory', priority: 900,
|
|
412
|
+
required: true, requiredMarkers: [slug, 'currentPhase'], selectionReason: 'Contribution review requires the current phase and integrity state.',
|
|
413
|
+
}),
|
|
414
|
+
sourceCandidate(projectRoot, resolve(buildRoot, 'test-plan.md'), {
|
|
415
|
+
id: 'test-plan', label: 'Current Test Plan', evidenceClass: 'relevant', priority: 70,
|
|
416
|
+
selectionReason: 'Accepted test challenges are relevant to evidence review.',
|
|
417
|
+
})].filter(Boolean);
|
|
418
|
+
}
|
|
419
|
+
throw new Error(`Unsupported context profile: ${input.profile}.`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function readStoredContextManifest(projectRoot, digestValue) {
|
|
423
|
+
const value = clean(digestValue);
|
|
424
|
+
if (!/^[a-f0-9]{64}$/.test(value)) throw new Error('Context pack digest must be a 64-character lowercase SHA-256 value.');
|
|
425
|
+
const path = resolve(contextRuntimeRoot(projectRoot), 'manifests', `${value}.json`);
|
|
426
|
+
if (!existsSync(path)) throw new Error(`Context pack manifest not found: ${value}.`);
|
|
427
|
+
const manifest = JSON.parse(readFileSync(path, 'utf8'));
|
|
428
|
+
if (manifest.schema !== 'ewai.context-pack/v1' || manifest.digest !== value || contextPackDigest(manifest) !== value) {
|
|
429
|
+
throw new Error(`Stored context pack manifest is invalid: ${value}.`);
|
|
430
|
+
}
|
|
431
|
+
return manifest;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function prepareProjectContext(projectRoot, input = {}) {
|
|
435
|
+
const root = resolve(projectRoot);
|
|
436
|
+
const focus = clean(input.focus);
|
|
437
|
+
if (focus.length > 2_000) throw new Error('Context focus must be 2,000 characters or fewer.');
|
|
438
|
+
const previousPack = input.previousDigest ? readStoredContextManifest(root, input.previousDigest) : null;
|
|
439
|
+
const candidates = profileCandidates(root, input);
|
|
440
|
+
const sourceRevision = digest(canonical(candidates.map(({ id, content }) => ({ id, content }))));
|
|
441
|
+
const pack = prepareContextPack({
|
|
442
|
+
profile: input.profile,
|
|
443
|
+
focus,
|
|
444
|
+
budgetTokens: input.budgetTokens,
|
|
445
|
+
repositoryRevision: input.repositoryRevision ?? sourceRevision,
|
|
446
|
+
deliveryRevision: input.deliveryRevision ?? sourceRevision,
|
|
447
|
+
candidates,
|
|
448
|
+
personaCatalogue: input.personaCatalogue,
|
|
449
|
+
previousPack,
|
|
450
|
+
previousDigest: input.previousDigest,
|
|
451
|
+
cacheRoot: resolve(contextRuntimeRoot(root), 'cache'),
|
|
452
|
+
});
|
|
453
|
+
const manifestRoot = resolve(contextRuntimeRoot(root), 'manifests');
|
|
454
|
+
mkdirSync(manifestRoot, { recursive: true });
|
|
455
|
+
writeFileSync(resolve(manifestRoot, `${pack.digest}.json`), `${JSON.stringify(safeContextManifest(pack), null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
456
|
+
return pack;
|
|
457
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { contextProfiles, estimateContextTokens, prepareContextPack } from './context-assembly.mjs';
|
|
3
|
+
|
|
4
|
+
function median(values) {
|
|
5
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
6
|
+
if (!sorted.length) return 0;
|
|
7
|
+
const middle = Math.floor(sorted.length / 2);
|
|
8
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function expandSegments(segments = []) {
|
|
12
|
+
return segments.map((segment) => ({
|
|
13
|
+
...segment,
|
|
14
|
+
content: String(segment.content ?? '').repeat(Math.max(1, Math.min(1_000, Number(segment.repeat ?? 1)))),
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function baselineContext(segments) {
|
|
19
|
+
return segments.map((segment) => `## ${segment.label} [${segment.id}]\n${segment.content}`).join('\n\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rounded(value) {
|
|
23
|
+
return Math.round(value * 100) / 100;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runContextBenchmark(fixtures = [], options = {}) {
|
|
27
|
+
const samples = Math.max(1, Math.min(100, Number(options.samples ?? 7)));
|
|
28
|
+
const thresholds = {
|
|
29
|
+
minimumReductionPercent: Number(options.minimumReductionPercent ?? 40),
|
|
30
|
+
maximumMedianPreparationMs: Number(options.maximumMedianPreparationMs ?? 75),
|
|
31
|
+
maximumPeakRssGrowthBytes: Number(options.maximumPeakRssGrowthBytes ?? 32 * 1024 * 1024),
|
|
32
|
+
};
|
|
33
|
+
const expectedProfiles = Object.keys(contextProfiles);
|
|
34
|
+
const fixtureProfiles = fixtures.map(({ profile }) => profile);
|
|
35
|
+
const durations = [];
|
|
36
|
+
const rssStart = process.memoryUsage().rss;
|
|
37
|
+
let peakRss = rssStart;
|
|
38
|
+
|
|
39
|
+
// Warm the JS paths before recording local measurements.
|
|
40
|
+
for (const fixture of fixtures) {
|
|
41
|
+
const segments = expandSegments(fixture.segments);
|
|
42
|
+
prepareContextPack({ ...fixture, candidates: segments, repositoryRevision: 'benchmark', deliveryRevision: 'benchmark' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const profiles = fixtures.map((fixture) => {
|
|
46
|
+
const segments = expandSegments(fixture.segments);
|
|
47
|
+
const baseline = estimateContextTokens(baselineContext(segments));
|
|
48
|
+
let pack = null;
|
|
49
|
+
for (let index = 0; index < samples; index += 1) {
|
|
50
|
+
const started = performance.now();
|
|
51
|
+
pack = prepareContextPack({ ...fixture, candidates: segments, repositoryRevision: 'benchmark', deliveryRevision: 'benchmark' });
|
|
52
|
+
durations.push(performance.now() - started);
|
|
53
|
+
peakRss = Math.max(peakRss, process.memoryUsage().rss);
|
|
54
|
+
}
|
|
55
|
+
const candidateTokens = pack.usage.estimatedInputTokens;
|
|
56
|
+
const reductionPercent = baseline.estimatedTokens
|
|
57
|
+
? ((baseline.estimatedTokens - candidateTokens) / baseline.estimatedTokens) * 100
|
|
58
|
+
: 0;
|
|
59
|
+
return {
|
|
60
|
+
profile: fixture.profile,
|
|
61
|
+
status: pack.status,
|
|
62
|
+
baseline: { bytes: baseline.bytes, estimatedTokens: baseline.estimatedTokens },
|
|
63
|
+
candidate: { bytes: pack.usage.estimatedInputBytes, estimatedTokens: candidateTokens },
|
|
64
|
+
reductionPercent: rounded(reductionPercent),
|
|
65
|
+
mandatoryRecall: pack.fidelity.mandatoryRecall,
|
|
66
|
+
fidelity: { status: pack.fidelity.status, requiredCount: pack.fidelity.requiredCount, presentCount: pack.fidelity.presentCount, missing: pack.fidelity.missing },
|
|
67
|
+
segmentCounts: {
|
|
68
|
+
selected: pack.segments.filter(({ disposition }) => disposition === 'selected').length,
|
|
69
|
+
reused: pack.segments.filter(({ disposition }) => disposition === 'reused').length,
|
|
70
|
+
deferred: pack.segments.filter(({ disposition }) => disposition === 'deferred').length,
|
|
71
|
+
overflow: pack.segments.filter(({ disposition }) => disposition === 'overflow').length,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const failures = [];
|
|
77
|
+
for (const profile of profiles) {
|
|
78
|
+
if (profile.status !== 'ready' || profile.fidelity.status !== 'pass' || profile.mandatoryRecall !== 1) {
|
|
79
|
+
failures.push({ code: 'mandatory-recall', profile: profile.profile, message: 'Mandatory evidence recall is below 100 percent.' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const profileCoverage = expectedProfiles.every((profile) => fixtureProfiles.includes(profile))
|
|
83
|
+
&& fixtureProfiles.every((profile) => expectedProfiles.includes(profile))
|
|
84
|
+
&& new Set(fixtureProfiles).size === expectedProfiles.length;
|
|
85
|
+
if (!profileCoverage) failures.push({ code: 'profile-coverage', message: 'Every supported profile must appear exactly once.' });
|
|
86
|
+
const medianReductionPercent = rounded(median(profiles.map(({ reductionPercent }) => reductionPercent)));
|
|
87
|
+
if (medianReductionPercent < thresholds.minimumReductionPercent) {
|
|
88
|
+
failures.push({ code: 'token-reduction', message: `Median reduction ${medianReductionPercent} is below ${thresholds.minimumReductionPercent}.` });
|
|
89
|
+
}
|
|
90
|
+
const medianPreparationMs = rounded(median(durations));
|
|
91
|
+
const peakRssGrowthBytes = Math.max(0, peakRss - rssStart);
|
|
92
|
+
if (medianPreparationMs > thresholds.maximumMedianPreparationMs) {
|
|
93
|
+
failures.push({ code: 'preparation-latency', message: `Median local preparation ${medianPreparationMs}ms exceeds ${thresholds.maximumMedianPreparationMs}ms.` });
|
|
94
|
+
}
|
|
95
|
+
if (peakRssGrowthBytes > thresholds.maximumPeakRssGrowthBytes) {
|
|
96
|
+
failures.push({ code: 'preparation-memory', message: `Peak RSS growth ${peakRssGrowthBytes} exceeds ${thresholds.maximumPeakRssGrowthBytes}.` });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
schema: 'ewai.context-benchmark/v1',
|
|
101
|
+
status: failures.length ? 'fail' : 'pass',
|
|
102
|
+
method: {
|
|
103
|
+
corpus: 'committed-equal-input-fixtures',
|
|
104
|
+
estimator: 'utf8-bytes-div-3-v1',
|
|
105
|
+
samples,
|
|
106
|
+
node: process.version,
|
|
107
|
+
platform: `${process.platform}-${process.arch}`,
|
|
108
|
+
},
|
|
109
|
+
thresholds,
|
|
110
|
+
profiles,
|
|
111
|
+
aggregate: { profileCoverage, medianReductionPercent },
|
|
112
|
+
performance: { medianPreparationMs, peakRssGrowthBytes },
|
|
113
|
+
failures,
|
|
114
|
+
};
|
|
115
|
+
}
|