@thebackstoryis/engineering-with-ai 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Docs/README.md +50 -0
- package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
- package/Docs/adoption/non-technical-team-guide.md +126 -0
- package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
- package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
- package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
- package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
- package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
- package/Docs/cli-reference.md +113 -0
- package/Docs/completed-phase-evidence-amendments.md +74 -0
- package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
- package/Docs/context-aware-delivery-companion-guide.md +198 -0
- package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
- package/Docs/context-management-and-token-efficiency.md +113 -0
- package/Docs/design-systems/design-system-implementation-guide.md +85 -0
- package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
- package/Docs/design-systems/design-system-review-guide.md +51 -0
- package/Docs/design-systems/design-system-user-guide.md +96 -0
- package/Docs/design-systems/product-owner-guide.md +49 -0
- package/Docs/designing-organisation-blueprint-packs.md +384 -0
- package/Docs/developer-delivery-guide.md +224 -0
- package/Docs/error-reporting-guide.md +110 -0
- package/Docs/error-reporting-provider-guide.md +49 -0
- package/Docs/examples/error-report-adapter.md +70 -0
- package/Docs/examples/meeting-review.md +76 -0
- package/Docs/examples/minimal-design-system.md +67 -0
- package/Docs/examples/prototype-review-inputs.md +175 -0
- package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
- package/Docs/examples/test-scenario-input.md +68 -0
- package/Docs/examples/worked-examples.md +147 -0
- package/Docs/existing-project-onboarding-guide.md +214 -0
- package/Docs/explanation/core-concepts.md +26 -0
- package/Docs/explanation/delivery-workflow.md +48 -0
- package/Docs/governance/governance-team-guide.md +139 -0
- package/Docs/governed-starter-project-materialisation-guide.md +284 -0
- package/Docs/guide-catalogue.md +117 -0
- package/Docs/guided-discovery-facilitator-guide.md +172 -0
- package/Docs/guided-intent-workspace-guide.md +109 -0
- package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
- package/Docs/human-approval-and-assurance-guide.md +146 -0
- package/Docs/knowledge-proposals-implementer-guide.md +106 -0
- package/Docs/knowledge-proposals-user-guide.md +247 -0
- package/Docs/maintainers/context-benchmarks.md +29 -0
- package/Docs/maintainers/contributing.md +58 -0
- package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
- package/Docs/maintainers/verification-walkthroughs.md +104 -0
- package/Docs/meeting-evidence-implementer-guide.md +132 -0
- package/Docs/meeting-evidence-user-guide.md +200 -0
- package/Docs/operations/dashboard-and-delivery-state.md +153 -0
- package/Docs/operations/dashboard-configuration.md +87 -0
- package/Docs/operations/installation-updating-and-entitlements.md +135 -0
- package/Docs/operations/premium-personas-setup.md +76 -0
- package/Docs/operations/troubleshooting-and-recovery.md +205 -0
- package/Docs/organisation-rollout-guide.md +142 -0
- package/Docs/persona-entitlement-provider-guide.md +199 -0
- package/Docs/persona-guided-prototype-iteration.md +129 -0
- package/Docs/personas/organisation-specific-personas.md +103 -0
- package/Docs/personas/persona-authoring-cookbook.md +176 -0
- package/Docs/personas/persona-engagement-ui.md +133 -0
- package/Docs/personas/persona-governance.md +118 -0
- package/Docs/platform-export-analysis-guide.md +336 -0
- package/Docs/policies/governance-owner-guide.md +36 -0
- package/Docs/policies/implementation-guide.md +42 -0
- package/Docs/policies/organisation-policy-design-gates.md +58 -0
- package/Docs/policies/policy-pack-authoring-guide.md +108 -0
- package/Docs/policies/product-owner-guide.md +43 -0
- package/Docs/policies/technical-owner-guide.md +37 -0
- package/Docs/product-owner-guide.md +327 -0
- package/Docs/project-portfolio-orchestration-guide.md +199 -0
- package/Docs/quality/manual-qa-and-acceptance.md +162 -0
- package/Docs/quality/persona-driven-test-scenarios.md +172 -0
- package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
- package/Docs/reference/capabilities-and-project-layout.md +678 -0
- package/Docs/reference/cli-and-configuration.md +398 -0
- package/Docs/reference/contributions-api.md +23 -0
- package/Docs/reference/security-adapter-authoring.md +81 -0
- package/Docs/reference/starter-adapter-authoring.md +74 -0
- package/Docs/repository-source-map-guide.md +381 -0
- package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
- package/Docs/screen-prototype-creation-guide.md +324 -0
- package/Docs/security-validation-guide.md +353 -0
- package/Docs/solution-readiness-review-guide.md +123 -0
- package/Docs/standards/project-standards-authoring.md +157 -0
- package/Docs/team-hub-guide.md +162 -0
- package/Docs/team-hub-resource-registry-guide.md +167 -0
- package/Docs/tutorials/first-delivery.md +83 -0
- package/Docs/tutorials/first-session.md +62 -0
- package/Docs/using-lifecycle-hooks.md +381 -0
- package/Docs/working-with-personas.md +274 -0
- package/LICENSE +165 -0
- package/README.md +96 -0
- package/agents-src/claude/ewai-security-reviewer.md +15 -0
- package/bin/ewai +5 -0
- package/config/archaeology-record-families.yaml +59 -0
- package/config/delivery-artifacts.yaml +121 -0
- package/config/delivery-stages.yaml +77 -0
- package/config/design-system.schema.json +46 -0
- package/config/error-reporting.schema.json +79 -0
- package/config/evidence-depth.schema.json +53 -0
- package/config/intent.schema.json +90 -0
- package/config/knowledge-proposals-proposal.schema.json +34 -0
- package/config/lifecycle-event.schema.json +68 -0
- package/config/lifecycle-handler.schema.json +45 -0
- package/config/lifecycle-hook-ack.schema.json +19 -0
- package/config/meeting-evidence-candidate.schema.json +102 -0
- package/config/organisation-policy.schema.json +137 -0
- package/config/pack.schema.json +250 -0
- package/config/persona-pack.schema.json +21 -0
- package/config/persona.schema.json +17 -0
- package/config/policy-evaluation.schema.json +77 -0
- package/config/policy-facts.schema.json +140 -0
- package/config/portfolio.schema.json +68 -0
- package/config/project.schema.json +313 -0
- package/config/prototype-iteration.schema.json +128 -0
- package/config/rollout.schema.json +87 -0
- package/config/security-adapter.schema.json +31 -0
- package/config/security-scan-request.schema.json +64 -0
- package/config/security-scan-response.schema.json +52 -0
- package/config/security-validation-policy.schema.json +74 -0
- package/config/starter-source-acknowledgement.schema.json +13 -0
- package/config/starter-source-adapter.schema.json +38 -0
- package/config/starter-source-request.schema.json +61 -0
- package/package.json +77 -0
- package/packs/core/pack.yaml +7 -0
- package/packs/design-systems/default/experience-promise.md +9 -0
- package/packs/design-systems/default/intentional-review.md +10 -0
- package/packs/design-systems/default/interaction-and-entry.md +9 -0
- package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
- package/packs/design-systems/default/pack.yaml +44 -0
- package/packs/design-systems/default/principles.md +10 -0
- package/packs/personas/core/pack.yaml +7 -0
- package/packs/personas/core/personas/archaeologist.md +37 -0
- package/packs/personas/core/personas/end-user.md +17 -0
- package/packs/personas/core/personas/maintainer.md +17 -0
- package/packs/personas/core/personas/operator.md +17 -0
- package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
- package/packs/technologies/laravel/pack.yaml +30 -0
- package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
- package/packs/technologies/nuxt/pack.yaml +30 -0
- package/packs/technologies/power-platform/pack.yaml +31 -0
- package/packs/technologies/salesforce/pack.yaml +25 -0
- package/public/app.js +4896 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/backstory-icon.png +0 -0
- package/public/dashboard-navigation.js +98 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +789 -0
- package/public/styles.css +2693 -0
- package/public/team-hub/app.js +202 -0
- package/public/team-hub/index.html +79 -0
- package/public/team-hub/styles.css +90 -0
- package/scripts/publication-check.mjs +140 -0
- package/scripts/setup.mjs +21 -0
- package/skills-src/ewai-archaeology/SKILL.md +334 -0
- package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
- package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
- package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
- package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
- package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
- package/skills-src/ewai-architecture/SKILL.md +108 -0
- package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
- package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
- package/skills-src/ewai-context/SKILL.md +68 -0
- package/skills-src/ewai-context/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/SKILL.md +118 -0
- package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
- package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
- package/skills-src/ewai-deliver/SKILL.md +122 -0
- package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
- package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
- package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
- package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
- package/skills-src/ewai-design-system-author/SKILL.md +28 -0
- package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
- package/skills-src/ewai-design-system-review/SKILL.md +26 -0
- package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
- package/skills-src/ewai-error-reporting/SKILL.md +46 -0
- package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
- package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
- package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
- package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
- package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
- package/skills-src/ewai-intent/SKILL.md +68 -0
- package/skills-src/ewai-intent/agents/openai.yaml +4 -0
- package/skills-src/ewai-intent/references/intent-contract.md +42 -0
- package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
- package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
- package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
- package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
- package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
- package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
- package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
- package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
- package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
- package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
- package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
- package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
- package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
- package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/SKILL.md +130 -0
- package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/references/cli.md +86 -0
- package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
- package/skills-src/ewai-portfolio/SKILL.md +70 -0
- package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
- package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
- package/skills-src/ewai-project-discovery/SKILL.md +95 -0
- package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
- package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
- package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
- package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
- package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
- package/skills-src/ewai-retro/SKILL.md +48 -0
- package/skills-src/ewai-retro/agents/openai.yaml +4 -0
- package/skills-src/ewai-retro/references/asset-routing.md +14 -0
- package/skills-src/ewai-rollout/SKILL.md +74 -0
- package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
- package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
- package/skills-src/ewai-shape-intents/SKILL.md +84 -0
- package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
- package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
- package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
- package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/SKILL.md +93 -0
- package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
- package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
- package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
- package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
- package/src/afk-worker.mjs +16 -0
- package/src/archaeology.mjs +1333 -0
- package/src/checkin.mjs +261 -0
- package/src/cli.mjs +2427 -0
- package/src/companion-guidance.mjs +257 -0
- package/src/companion-opening.mjs +62 -0
- package/src/companion.mjs +256 -0
- package/src/context.mjs +210 -0
- package/src/dashboard-preferences.mjs +80 -0
- package/src/delivery-artifacts.mjs +204 -0
- package/src/delivery-documents.mjs +248 -0
- package/src/delivery-gates.mjs +317 -0
- package/src/delivery.mjs +1433 -0
- package/src/design-system-application.mjs +291 -0
- package/src/design-system-authoring.mjs +101 -0
- package/src/design-systems.mjs +466 -0
- package/src/discovery.mjs +1314 -0
- package/src/error-reporting.mjs +323 -0
- package/src/evidence-depth.mjs +543 -0
- package/src/execution-state.mjs +243 -0
- package/src/install.mjs +166 -0
- package/src/intent-dependencies.mjs +117 -0
- package/src/intent-maps.mjs +402 -0
- package/src/intents.mjs +747 -0
- package/src/knowledge-proposals.mjs +717 -0
- package/src/launcher.mjs +51 -0
- package/src/meeting-evidence.mjs +703 -0
- package/src/network-rollout.mjs +386 -0
- package/src/organisation-blueprints.mjs +438 -0
- package/src/organisation-policies.mjs +448 -0
- package/src/packs.mjs +44 -0
- package/src/paths.mjs +62 -0
- package/src/persona-entitlements.mjs +438 -0
- package/src/persona-licence-config.mjs +98 -0
- package/src/persona-website-provider.mjs +134 -0
- package/src/persona-zip.mjs +87 -0
- package/src/personas.mjs +159 -0
- package/src/platform-metadata-analysis.mjs +314 -0
- package/src/policy-design-gates.mjs +623 -0
- package/src/policy-gate-integration.mjs +318 -0
- package/src/portfolio.mjs +509 -0
- package/src/power-platform-source-map.mjs +190 -0
- package/src/project.mjs +449 -0
- package/src/prototype-iterations.mjs +730 -0
- package/src/repository-source-map.mjs +603 -0
- package/src/runtime/afk-conductor.mjs +973 -0
- package/src/runtime/context-assembly.mjs +457 -0
- package/src/runtime/context-benchmarks.mjs +115 -0
- package/src/runtime/dashboard-actions.mjs +109 -0
- package/src/runtime/dashboard-handoffs.mjs +149 -0
- package/src/runtime/dashboard-server.mjs +1272 -0
- package/src/runtime/dashboard.mjs +197 -0
- package/src/runtime/database.mjs +789 -0
- package/src/runtime/error-reporting.mjs +581 -0
- package/src/runtime/evidence-depth-workspace.mjs +412 -0
- package/src/runtime/execution-leases.mjs +299 -0
- package/src/runtime/guided-discovery.mjs +350 -0
- package/src/runtime/guided-intents.mjs +517 -0
- package/src/runtime/impact-analysis.mjs +535 -0
- package/src/runtime/intents.mjs +222 -0
- package/src/runtime/knowledge.mjs +86 -0
- package/src/runtime/lifecycle-hooks.mjs +1239 -0
- package/src/runtime/mcp-config.mjs +110 -0
- package/src/runtime/mcp-server.mjs +1885 -0
- package/src/runtime/palace.mjs +362 -0
- package/src/runtime/paths.mjs +58 -0
- package/src/runtime/persona-engagement.mjs +255 -0
- package/src/runtime/phase-contributions.mjs +594 -0
- package/src/runtime/policy-workspace.mjs +170 -0
- package/src/runtime/prototype-iterations.mjs +235 -0
- package/src/runtime/provider-adapters.mjs +163 -0
- package/src/runtime/repository-index.mjs +838 -0
- package/src/runtime/runs.mjs +185 -0
- package/src/runtime/security-validation.mjs +1230 -0
- package/src/runtime/starter-materialisation.mjs +1155 -0
- package/src/runtime/team-hub-client.mjs +479 -0
- package/src/runtime/team-hub-database.mjs +288 -0
- package/src/runtime/team-hub-server.mjs +191 -0
- package/src/runtime/team-hub.mjs +110 -0
- package/src/runtime/tree-sitter-index.mjs +390 -0
- package/src/runtime/version.mjs +1 -0
- package/src/runtime/work.mjs +633 -0
- package/src/salesforce-source-map.mjs +212 -0
- package/src/security-validation-config.mjs +224 -0
- package/src/solution-readiness.mjs +620 -0
- package/src/starter-materialisation-contract.mjs +407 -0
- package/src/task-graph.mjs +544 -0
- package/src/team-hub-resources.mjs +239 -0
- package/src/team-hub.mjs +242 -0
- package/src/test-scenarios.mjs +622 -0
- package/src/validation-config.mjs +289 -0
- package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
- package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
- package/templates/SPECS/6.Build/_tracker-template.md +16 -0
- package/templates/SPECS/pipeline.yaml +62 -0
- package/templates/discovery-answers.yaml +86 -0
- package/templates/intent-body.md +21 -0
- package/tests/fixtures/context-benchmarks.json +9 -0
package/src/intents.mjs
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { loadProjectConfig } from './project.mjs';
|
|
6
|
+
import { validatePersonaRef } from './personas.mjs';
|
|
7
|
+
import { publishLifecycleEventSafely } from './runtime/lifecycle-hooks.mjs';
|
|
8
|
+
|
|
9
|
+
const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const intentReferencePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11
|
+
export const intentRelationshipTypes = Object.freeze([
|
|
12
|
+
'depends-on',
|
|
13
|
+
'enables',
|
|
14
|
+
'complements',
|
|
15
|
+
'conflicts-with',
|
|
16
|
+
'supersedes',
|
|
17
|
+
'relates-to',
|
|
18
|
+
]);
|
|
19
|
+
const deliveryShapeRecommendations = Object.freeze(['single', 'split', 'decision-required']);
|
|
20
|
+
const intentMutationQueues = new Map();
|
|
21
|
+
const processInstanceId = randomUUID();
|
|
22
|
+
|
|
23
|
+
function intentTransactionRoot(projectRoot) {
|
|
24
|
+
return resolve(projectRoot, '.ewai-pipeline/runtime/intent-transactions');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeTransactionJournal(path, value) {
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
29
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
30
|
+
try {
|
|
31
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
32
|
+
renameSync(temporary, path);
|
|
33
|
+
} finally {
|
|
34
|
+
rmSync(temporary, { force: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateTransactionEntry(entry, intentsRoot) {
|
|
39
|
+
const path = resolve(String(entry.path ?? ''));
|
|
40
|
+
const rel = relative(intentsRoot, path);
|
|
41
|
+
if (
|
|
42
|
+
rel.startsWith('..')
|
|
43
|
+
|| resolve(intentsRoot, rel) !== path
|
|
44
|
+
|| !['.md', '.json'].some((extension) => path.endsWith(extension))
|
|
45
|
+
) {
|
|
46
|
+
throw new Error(`Intent transaction target is outside the intent library: ${path}`);
|
|
47
|
+
}
|
|
48
|
+
const temporary = String(entry.temporary ?? '');
|
|
49
|
+
const backup = String(entry.backup ?? '');
|
|
50
|
+
if (!temporary.startsWith(`${path}.ewai-`) || !temporary.endsWith('.tmp')) {
|
|
51
|
+
throw new Error(`Intent transaction has an invalid temporary path: ${temporary}`);
|
|
52
|
+
}
|
|
53
|
+
if (!backup.startsWith(`${path}.ewai-`) || !backup.endsWith('.bak')) {
|
|
54
|
+
throw new Error(`Intent transaction has an invalid backup path: ${backup}`);
|
|
55
|
+
}
|
|
56
|
+
return { path, temporary, backup, hadOriginal: entry.hadOriginal === true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rollbackIntentTransaction(journalPath, intentsRoot) {
|
|
60
|
+
const journal = JSON.parse(readFileSync(journalPath, 'utf8'));
|
|
61
|
+
if (journal.schema !== 'ewai.intent-transaction/v1' || !Array.isArray(journal.entries)) {
|
|
62
|
+
throw new Error(`Invalid intent transaction journal: ${journalPath}`);
|
|
63
|
+
}
|
|
64
|
+
const entries = journal.entries.map((entry) => validateTransactionEntry(entry, intentsRoot));
|
|
65
|
+
for (const entry of entries.reverse()) {
|
|
66
|
+
if (existsSync(entry.backup)) {
|
|
67
|
+
rmSync(entry.path, { force: true });
|
|
68
|
+
renameSync(entry.backup, entry.path);
|
|
69
|
+
} else if (!entry.hadOriginal) {
|
|
70
|
+
rmSync(entry.path, { force: true });
|
|
71
|
+
}
|
|
72
|
+
rmSync(entry.temporary, { force: true });
|
|
73
|
+
}
|
|
74
|
+
rmSync(journalPath, { force: true });
|
|
75
|
+
return journal.id;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function recoverIntentTransactions(projectRoot) {
|
|
79
|
+
const root = intentTransactionRoot(projectRoot);
|
|
80
|
+
if (!existsSync(root)) return { recovered: [] };
|
|
81
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
82
|
+
const intentsRoot = resolve(paths.specsRoot, '2.Purpose/intents');
|
|
83
|
+
const recovered = [];
|
|
84
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
85
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
|
86
|
+
const journalPath = resolve(root, entry.name);
|
|
87
|
+
const journal = JSON.parse(readFileSync(journalPath, 'utf8'));
|
|
88
|
+
if (journal.owner === processInstanceId) continue;
|
|
89
|
+
if (Number.isInteger(journal.pid) && journal.pid > 0 && journal.pid !== process.pid) {
|
|
90
|
+
try {
|
|
91
|
+
process.kill(journal.pid, 0);
|
|
92
|
+
continue;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.code !== 'ESRCH') continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
recovered.push(rollbackIntentTransaction(journalPath, intentsRoot));
|
|
98
|
+
}
|
|
99
|
+
return { recovered };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function atomicWriteSet(projectRoot, entries) {
|
|
103
|
+
const transaction = randomUUID();
|
|
104
|
+
const journalPath = resolve(intentTransactionRoot(projectRoot), `${transaction}.json`);
|
|
105
|
+
const staged = [];
|
|
106
|
+
const committed = [];
|
|
107
|
+
let succeeded = false;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
for (const { path, content } of entries) {
|
|
111
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
112
|
+
const temporary = `${path}.ewai-${process.pid}-${transaction}.tmp`;
|
|
113
|
+
const backup = `${path}.ewai-${process.pid}-${transaction}.bak`;
|
|
114
|
+
staged.push({ path, temporary, backup, hadOriginal: existsSync(path) });
|
|
115
|
+
writeFileSync(temporary, content, 'utf8');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
writeTransactionJournal(journalPath, {
|
|
119
|
+
schema: 'ewai.intent-transaction/v1',
|
|
120
|
+
id: transaction,
|
|
121
|
+
owner: processInstanceId,
|
|
122
|
+
pid: process.pid,
|
|
123
|
+
entries: staged.map(({ path, temporary, backup, hadOriginal }) => ({
|
|
124
|
+
path,
|
|
125
|
+
temporary,
|
|
126
|
+
backup,
|
|
127
|
+
hadOriginal,
|
|
128
|
+
})),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
for (const { path, backup, hadOriginal } of staged) {
|
|
132
|
+
if (hadOriginal) renameSync(path, backup);
|
|
133
|
+
}
|
|
134
|
+
for (const { path, temporary } of staged) {
|
|
135
|
+
renameSync(temporary, path);
|
|
136
|
+
committed.push(path);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
rmSync(journalPath);
|
|
140
|
+
succeeded = true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (existsSync(journalPath)) {
|
|
143
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
144
|
+
rollbackIntentTransaction(
|
|
145
|
+
journalPath,
|
|
146
|
+
resolve(paths.specsRoot, '2.Purpose/intents'),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
else for (const path of committed.reverse()) rmSync(path, { force: true });
|
|
150
|
+
throw error;
|
|
151
|
+
} finally {
|
|
152
|
+
for (const { temporary } of staged) rmSync(temporary, { force: true });
|
|
153
|
+
if (succeeded) {
|
|
154
|
+
for (const { backup } of staged) rmSync(backup, { force: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function intentStatePath(path) {
|
|
160
|
+
return path.replace(/\.md$/i, '.json');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseIntentDocument(path) {
|
|
164
|
+
const content = readFileSync(path, 'utf8');
|
|
165
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n[\s\S]*)$/);
|
|
166
|
+
if (!match) throw new Error(`Intent has no valid YAML frontmatter: ${path}`);
|
|
167
|
+
return { content, metadata: YAML.parse(match[1]) ?? {}, body: match[2] };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function structuredIntentState(projectRoot, path, metadata) {
|
|
171
|
+
return {
|
|
172
|
+
schema: 'ewai.intent-state/v1',
|
|
173
|
+
slug: metadata.slug,
|
|
174
|
+
domain: metadata.domain,
|
|
175
|
+
title: metadata.title,
|
|
176
|
+
status: metadata.status,
|
|
177
|
+
deliveryStatus: metadata.delivery_status,
|
|
178
|
+
currentPhase: metadata.current_phase,
|
|
179
|
+
deliveryStatePath: metadata.delivery_state_path ?? null,
|
|
180
|
+
intentMap: metadata.intent_map ?? null,
|
|
181
|
+
personas: Array.isArray(metadata.personas) ? metadata.personas : [],
|
|
182
|
+
relationships: Array.isArray(metadata.relationships) ? metadata.relationships : [],
|
|
183
|
+
deliveryShape: metadata.delivery_shape ?? null,
|
|
184
|
+
updatedAt: metadata.updated_at,
|
|
185
|
+
markdownPath: relative(resolve(projectRoot), path).replaceAll('\\', '/')
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function asMarkdown(value) {
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
return value.length ? value.map((item) => `- ${String(item).trim()}`).join('\n') : '';
|
|
192
|
+
}
|
|
193
|
+
return String(value ?? '').trim();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validatedRelationship(value) {
|
|
197
|
+
const type = String(value?.type ?? '').trim();
|
|
198
|
+
const target = String(value?.target ?? '').trim();
|
|
199
|
+
const rationale = String(value?.rationale ?? '').trim();
|
|
200
|
+
const suppliedRequiredBefore = String(value?.required_before ?? '').trim();
|
|
201
|
+
const suppliedRequiredState = String(value?.required_state ?? '').trim();
|
|
202
|
+
if (!intentRelationshipTypes.includes(type)) {
|
|
203
|
+
throw new Error(`Intent relationship type must be one of: ${intentRelationshipTypes.join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
if (!intentReferencePattern.test(target)) {
|
|
206
|
+
throw new Error(`Intent relationship target must be an exact domain/slug reference: ${target}`);
|
|
207
|
+
}
|
|
208
|
+
if ((suppliedRequiredBefore || suppliedRequiredState) && type !== 'depends-on') {
|
|
209
|
+
throw new Error('required_before and required_state apply only to depends-on relationships');
|
|
210
|
+
}
|
|
211
|
+
const requiredBefore = type === 'depends-on' ? suppliedRequiredBefore || 'delivery' : '';
|
|
212
|
+
const requiredState = type === 'depends-on' ? suppliedRequiredState || 'delivered' : '';
|
|
213
|
+
if (requiredBefore && !['build', 'delivery'].includes(requiredBefore)) {
|
|
214
|
+
throw new Error('depends-on required_before must be build or delivery');
|
|
215
|
+
}
|
|
216
|
+
if (requiredState && !['plan-complete', 'delivered'].includes(requiredState)) {
|
|
217
|
+
throw new Error('depends-on required_state must be plan-complete or delivered');
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
type,
|
|
221
|
+
target,
|
|
222
|
+
...(rationale ? { rationale } : {}),
|
|
223
|
+
...(requiredBefore ? { required_before: requiredBefore } : {}),
|
|
224
|
+
...(requiredState ? { required_state: requiredState } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function validatedDeliveryShape(value) {
|
|
229
|
+
if (value == null) return null;
|
|
230
|
+
const recommendation = String(value.recommendation ?? '').trim();
|
|
231
|
+
if (!deliveryShapeRecommendations.includes(recommendation)) {
|
|
232
|
+
throw new Error(`Delivery-shape recommendation must be one of: ${deliveryShapeRecommendations.join(', ')}`);
|
|
233
|
+
}
|
|
234
|
+
const reason = String(value.reason ?? '').trim();
|
|
235
|
+
if (!reason) throw new Error('Delivery-shape preview requires a reason');
|
|
236
|
+
const suggestedChildren = (value.suggested_children ?? value.suggestedChildren ?? []).map((child) => {
|
|
237
|
+
const slug = String(child?.slug ?? '').trim();
|
|
238
|
+
if (slug && !slugPattern.test(slug)) {
|
|
239
|
+
throw new Error(`Suggested child slug must be lower kebab-case: ${slug}`);
|
|
240
|
+
}
|
|
241
|
+
const domain = String(child?.domain ?? '').trim();
|
|
242
|
+
if (domain && !slugPattern.test(domain)) {
|
|
243
|
+
throw new Error(`Suggested child domain must be lower kebab-case: ${domain}`);
|
|
244
|
+
}
|
|
245
|
+
const title = String(child?.title ?? '').trim();
|
|
246
|
+
const outcome = String(child?.outcome ?? '').trim();
|
|
247
|
+
const dependsOn = Array.isArray(child?.depends_on ?? child?.dependsOn)
|
|
248
|
+
? (child.depends_on ?? child.dependsOn).map((item) => String(item).trim()).filter(Boolean)
|
|
249
|
+
: [];
|
|
250
|
+
return {
|
|
251
|
+
...(domain ? { domain } : {}),
|
|
252
|
+
...(slug ? { slug } : {}),
|
|
253
|
+
...(title ? { title } : {}),
|
|
254
|
+
outcome,
|
|
255
|
+
...(dependsOn.length ? { depends_on: dependsOn } : {}),
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
const blockingQuestions = Array.isArray(value.blocking_questions ?? value.blockingQuestions)
|
|
259
|
+
? (value.blocking_questions ?? value.blockingQuestions).map((item) => String(item).trim()).filter(Boolean)
|
|
260
|
+
: [];
|
|
261
|
+
const reviewedDecision = String(value.reviewed_decision ?? value.reviewedDecision ?? '').trim();
|
|
262
|
+
if (reviewedDecision && !['keep-as-one', 'split', 'refine-split', 'defer'].includes(reviewedDecision)) {
|
|
263
|
+
throw new Error('Delivery-shape reviewed_decision must be keep-as-one, split, refine-split, or defer');
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
recommendation,
|
|
267
|
+
reason,
|
|
268
|
+
suggested_children: suggestedChildren,
|
|
269
|
+
blocking_questions: blockingQuestions,
|
|
270
|
+
reviewed_decision: reviewedDecision || null,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function renderDeliveryShape(deliveryShape) {
|
|
275
|
+
if (!deliveryShape) return '';
|
|
276
|
+
const children = deliveryShape.suggested_children.length
|
|
277
|
+
? deliveryShape.suggested_children.map((child) => {
|
|
278
|
+
const label = child.slug
|
|
279
|
+
? `\`${child.domain ? `${child.domain}/` : ''}${child.slug}\``
|
|
280
|
+
: child.title || 'Suggested child intent';
|
|
281
|
+
const dependencies = child.depends_on?.length ? ` Depends on: ${child.depends_on.join(', ')}.` : '';
|
|
282
|
+
return `- ${label}: ${child.outcome}${dependencies}`;
|
|
283
|
+
}).join('\n')
|
|
284
|
+
: '- No child intents suggested.';
|
|
285
|
+
const questions = deliveryShape.blocking_questions.length
|
|
286
|
+
? deliveryShape.blocking_questions.map((question) => `- ${question}`).join('\n')
|
|
287
|
+
: '- No blocking split questions recorded.';
|
|
288
|
+
return [
|
|
289
|
+
`- **Recommendation:** ${deliveryShape.recommendation}`,
|
|
290
|
+
`- **Reason:** ${deliveryShape.reason}`,
|
|
291
|
+
`- **Reviewed decision:** ${deliveryShape.reviewed_decision ?? 'not-reviewed'}`,
|
|
292
|
+
'',
|
|
293
|
+
'### Suggested child intents',
|
|
294
|
+
'',
|
|
295
|
+
children,
|
|
296
|
+
'',
|
|
297
|
+
'### Blocking split questions',
|
|
298
|
+
'',
|
|
299
|
+
questions,
|
|
300
|
+
].join('\n');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderIntentBody(title, details = {}, relationships = [], deliveryShape = null) {
|
|
304
|
+
const relationshipContent = relationships.map((relationship) => (
|
|
305
|
+
`- **${relationship.type}:** \`${relationship.target}\`${
|
|
306
|
+
relationship.rationale ? ` — ${relationship.rationale}` : ''
|
|
307
|
+
}${relationship.required_before ? ` (requires ${relationship.required_state ?? 'delivered'} before ${relationship.required_before})` : ''}`
|
|
308
|
+
)).join('\n');
|
|
309
|
+
const sections = [
|
|
310
|
+
['Problem', details.problem],
|
|
311
|
+
['Desired outcome', details.desiredOutcome],
|
|
312
|
+
['Users and personas', details.users],
|
|
313
|
+
['Journeys', details.journeys],
|
|
314
|
+
['Acceptance criteria', details.acceptanceCriteria],
|
|
315
|
+
['Constraints', details.constraints],
|
|
316
|
+
['Dependencies and relationships', relationshipContent],
|
|
317
|
+
['Delivery shape preview', renderDeliveryShape(deliveryShape)],
|
|
318
|
+
['Evidence', details.evidence],
|
|
319
|
+
['Open decisions', details.openDecisions],
|
|
320
|
+
];
|
|
321
|
+
return `# ${title}\n\n${sections.map(([heading, value]) => (
|
|
322
|
+
`## ${heading}\n\n${asMarkdown(value)}`
|
|
323
|
+
)).join('\n\n')}\n`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function parseIntentBodyDetails(body) {
|
|
327
|
+
const matches = [...String(body ?? '').matchAll(/^##\s+(.+)\r?\n\r?\n([\s\S]*?)(?=\r?\n##\s+|$)/gm)];
|
|
328
|
+
const sections = new Map(matches.map((match) => [match[1].trim(), match[2].trim()]));
|
|
329
|
+
const value = (heading) => sections.get(heading) ?? '';
|
|
330
|
+
return {
|
|
331
|
+
problem: value('Problem'),
|
|
332
|
+
desiredOutcome: value('Desired outcome'),
|
|
333
|
+
users: value('Users and personas'),
|
|
334
|
+
journeys: value('Journeys'),
|
|
335
|
+
acceptanceCriteria: value('Acceptance criteria'),
|
|
336
|
+
constraints: value('Constraints'),
|
|
337
|
+
evidence: value('Evidence'),
|
|
338
|
+
openDecisions: value('Open decisions'),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function parsePersonaAttachment(value) {
|
|
343
|
+
const [ref, role = 'primary', depthValue = '3'] = value.split(':');
|
|
344
|
+
const depth = Number(depthValue);
|
|
345
|
+
validatePersonaRef(ref);
|
|
346
|
+
if (!slugPattern.test(role)) throw new Error(`Invalid persona role: ${role}`);
|
|
347
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > 5) {
|
|
348
|
+
throw new Error(`Persona depth must be an integer from 1 to 5: ${depthValue}`);
|
|
349
|
+
}
|
|
350
|
+
return { ref, role, depth };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function createIntent(projectRoot, options) {
|
|
354
|
+
recoverIntentTransactions(projectRoot);
|
|
355
|
+
const slug = options.slug ?? '';
|
|
356
|
+
const domain = options.domain ?? 'general';
|
|
357
|
+
if (!slugPattern.test(slug)) throw new Error(`Intent slug must be lower kebab-case: ${slug}`);
|
|
358
|
+
if (!slugPattern.test(domain)) throw new Error(`Intent domain must be lower kebab-case: ${domain}`);
|
|
359
|
+
|
|
360
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
361
|
+
const path = resolve(paths.specsRoot, '2.Purpose/intents', domain, `${slug}.md`);
|
|
362
|
+
if (existsSync(path) && !options.force) throw new Error(`Intent already exists: ${path}`);
|
|
363
|
+
const intentsRoot = resolve(paths.specsRoot, '2.Purpose/intents');
|
|
364
|
+
const slugCollision = intentMarkdownFiles(intentsRoot).find((candidate) => {
|
|
365
|
+
if (candidate === path) return false;
|
|
366
|
+
try {
|
|
367
|
+
return String(parseIntentDocument(candidate).metadata.slug ?? '').trim() === slug;
|
|
368
|
+
} catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
if (slugCollision) {
|
|
373
|
+
throw new Error(`Intent slugs must be project-wide unique because delivery folders are slug-keyed: ${slug} already exists at ${relative(paths.projectRoot, slugCollision)}.`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const title = options.title || slug.split('-').map((part) => `${part[0].toUpperCase()}${part.slice(1)}`).join(' ');
|
|
377
|
+
const personas = (options.personas ?? []).map((persona) => validatedAttachment(
|
|
378
|
+
typeof persona === 'string' ? parsePersonaAttachment(persona) : persona
|
|
379
|
+
));
|
|
380
|
+
const rawIntentMap = options.intentMap == null ? null : String(options.intentMap).trim();
|
|
381
|
+
const intentMap = rawIntentMap || null;
|
|
382
|
+
if (intentMap && !slugPattern.test(intentMap)) {
|
|
383
|
+
throw new Error(`Intent map reference must be lower kebab-case: ${intentMap}`);
|
|
384
|
+
}
|
|
385
|
+
const relationships = (options.relationships ?? []).map(validatedRelationship);
|
|
386
|
+
const deliveryShape = validatedDeliveryShape(options.deliveryShape ?? options.delivery_shape ?? null);
|
|
387
|
+
const ownReference = `${domain}/${slug}`;
|
|
388
|
+
if (relationships.some((relationship) => relationship.target === ownReference)) {
|
|
389
|
+
throw new Error(`Intent cannot relate to itself: ${ownReference}`);
|
|
390
|
+
}
|
|
391
|
+
const allowedRelationshipTargets = new Set(options.allowedRelationshipTargets ?? []);
|
|
392
|
+
for (const relationship of relationships) {
|
|
393
|
+
const [targetDomain, targetSlug] = relationship.target.split('/');
|
|
394
|
+
const targetRoot = resolve(paths.specsRoot, '2.Purpose/intents', targetDomain, targetSlug);
|
|
395
|
+
if (!allowedRelationshipTargets.has(relationship.target)
|
|
396
|
+
&& !existsSync(`${targetRoot}.md`)
|
|
397
|
+
&& !existsSync(`${targetRoot}.json`)) {
|
|
398
|
+
throw new Error(`Intent ${ownReference} relates to missing intent ${relationship.target}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const metadata = {
|
|
402
|
+
schema: 'ewai.intent/v1',
|
|
403
|
+
slug,
|
|
404
|
+
domain,
|
|
405
|
+
title,
|
|
406
|
+
status: 'draft',
|
|
407
|
+
delivery_status: 'not-started',
|
|
408
|
+
current_phase: 'backlog',
|
|
409
|
+
delivery_state_path: null,
|
|
410
|
+
updated_at: new Date().toISOString(),
|
|
411
|
+
intent_map: intentMap,
|
|
412
|
+
personas,
|
|
413
|
+
relationships,
|
|
414
|
+
delivery_shape: deliveryShape,
|
|
415
|
+
};
|
|
416
|
+
const body = renderIntentBody(title, options.details, relationships, deliveryShape);
|
|
417
|
+
const content = `---\n${YAML.stringify(metadata).trimEnd()}\n---\n\n${body}`;
|
|
418
|
+
|
|
419
|
+
atomicWriteSet(paths.projectRoot, [
|
|
420
|
+
{ path, content },
|
|
421
|
+
{
|
|
422
|
+
path: intentStatePath(path),
|
|
423
|
+
content: `${JSON.stringify(structuredIntentState(paths.projectRoot, path, metadata), null, 2)}\n`,
|
|
424
|
+
},
|
|
425
|
+
]);
|
|
426
|
+
publishLifecycleEventSafely(paths.projectRoot, 'ewai.intent.created', {
|
|
427
|
+
sourceKey: `intent.created:${domain}/${slug}`,
|
|
428
|
+
occurredAt: metadata.updated_at,
|
|
429
|
+
sourceRevision: metadata.updated_at,
|
|
430
|
+
scope: { intent: `${domain}/${slug}` },
|
|
431
|
+
facts: { status: metadata.status, domain, slug },
|
|
432
|
+
evidence: [relative(paths.projectRoot, path).replaceAll('\\', '/')],
|
|
433
|
+
personas,
|
|
434
|
+
streamId: `intent:${domain}/${slug}`,
|
|
435
|
+
now: metadata.updated_at,
|
|
436
|
+
});
|
|
437
|
+
return { slug, domain, title, path, intentMap, personas, relationships };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function updateIntentDeliveryState(projectRoot, path, input = {}) {
|
|
441
|
+
const root = resolve(projectRoot);
|
|
442
|
+
recoverIntentTransactions(root);
|
|
443
|
+
const absolute = resolve(path);
|
|
444
|
+
if (!absolute.endsWith('.md') || relative(root, absolute).startsWith('..') || !existsSync(absolute)) {
|
|
445
|
+
throw new Error(`Intent Markdown is outside the project or missing: ${path}`);
|
|
446
|
+
}
|
|
447
|
+
const document = parseIntentDocument(absolute);
|
|
448
|
+
if (input.status) document.metadata.status = input.status;
|
|
449
|
+
if (input.deliveryStatus) document.metadata.delivery_status = input.deliveryStatus;
|
|
450
|
+
if (input.currentPhase) document.metadata.current_phase = input.currentPhase;
|
|
451
|
+
if (input.deliveryStatePath) document.metadata.delivery_state_path = input.deliveryStatePath;
|
|
452
|
+
document.metadata.updated_at = new Date().toISOString();
|
|
453
|
+
const structured = structuredIntentState(root, absolute, document.metadata);
|
|
454
|
+
atomicWriteSet(root, [
|
|
455
|
+
{
|
|
456
|
+
path: absolute,
|
|
457
|
+
content: `---\n${YAML.stringify(document.metadata).trimEnd()}\n---${document.body}`,
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
path: intentStatePath(absolute),
|
|
461
|
+
content: `${JSON.stringify(structured, null, 2)}\n`,
|
|
462
|
+
},
|
|
463
|
+
]);
|
|
464
|
+
return { markdownPath: absolute, jsonPath: intentStatePath(absolute), state: structured };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function validateIntentStateCopies(projectRoot, path, expected = {}) {
|
|
468
|
+
const root = resolve(projectRoot);
|
|
469
|
+
recoverIntentTransactions(root);
|
|
470
|
+
const absolute = resolve(path);
|
|
471
|
+
const document = parseIntentDocument(absolute);
|
|
472
|
+
const jsonPath = intentStatePath(absolute);
|
|
473
|
+
if (!existsSync(jsonPath)) throw new Error(`Intent JSON state is missing: ${relative(root, jsonPath)}`);
|
|
474
|
+
const structured = JSON.parse(readFileSync(jsonPath, 'utf8'));
|
|
475
|
+
const comparisons = {
|
|
476
|
+
status: document.metadata.status,
|
|
477
|
+
deliveryStatus: document.metadata.delivery_status,
|
|
478
|
+
currentPhase: document.metadata.current_phase,
|
|
479
|
+
deliveryStatePath: document.metadata.delivery_state_path ?? null
|
|
480
|
+
};
|
|
481
|
+
for (const [key, value] of Object.entries(comparisons)) {
|
|
482
|
+
if (structured[key] !== value) throw new Error(`Intent state drift: Markdown and JSON disagree on ${key}.`);
|
|
483
|
+
if (expected[key] !== undefined && expected[key] !== value) {
|
|
484
|
+
throw new Error(`Intent state drift: delivery state and intent copies disagree on ${key}.`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const [key, value] of Object.entries({
|
|
488
|
+
intentMap: document.metadata.intent_map ?? null,
|
|
489
|
+
personas: Array.isArray(document.metadata.personas) ? document.metadata.personas : [],
|
|
490
|
+
relationships: Array.isArray(document.metadata.relationships) ? document.metadata.relationships : [],
|
|
491
|
+
deliveryShape: document.metadata.delivery_shape ?? null,
|
|
492
|
+
})) {
|
|
493
|
+
if (JSON.stringify(structured[key]) !== JSON.stringify(value)) {
|
|
494
|
+
throw new Error(`Intent state drift: Markdown and JSON disagree on ${key}.`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { markdown: comparisons, json: structured };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function readDraftIntentForReconciliation(projectRoot, reference) {
|
|
501
|
+
const root = resolve(projectRoot);
|
|
502
|
+
recoverIntentTransactions(root);
|
|
503
|
+
const path = intentSource(root, reference);
|
|
504
|
+
const document = parseIntentDocument(path);
|
|
505
|
+
if (document.metadata.schema !== 'ewai.intent/v1') {
|
|
506
|
+
throw new Error(`Intent cannot be reconciled because its schema is unsupported: ${reference}`);
|
|
507
|
+
}
|
|
508
|
+
const checked = validateIntentStateCopies(root, path);
|
|
509
|
+
const structured = checked.json;
|
|
510
|
+
for (const [key, value] of Object.entries({
|
|
511
|
+
slug: document.metadata.slug,
|
|
512
|
+
domain: document.metadata.domain,
|
|
513
|
+
title: document.metadata.title,
|
|
514
|
+
updatedAt: document.metadata.updated_at,
|
|
515
|
+
markdownPath: relative(root, path).replaceAll('\\', '/'),
|
|
516
|
+
})) {
|
|
517
|
+
if (structured[key] !== value) {
|
|
518
|
+
throw new Error(`Intent cannot be reconciled because Markdown and JSON disagree on ${key}.`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const eligible = document.metadata.status === 'draft'
|
|
522
|
+
&& document.metadata.delivery_status === 'not-started'
|
|
523
|
+
&& document.metadata.current_phase === 'backlog'
|
|
524
|
+
&& !document.metadata.delivery_state_path;
|
|
525
|
+
if (!eligible) {
|
|
526
|
+
throw new Error(`Intent cannot be reconciled after governed delivery has started: ${reference}`);
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
reference,
|
|
530
|
+
sourceUpdatedAt: document.metadata.updated_at,
|
|
531
|
+
intent: {
|
|
532
|
+
domain: document.metadata.domain,
|
|
533
|
+
slug: document.metadata.slug,
|
|
534
|
+
title: document.metadata.title,
|
|
535
|
+
intentMap: document.metadata.intent_map ?? null,
|
|
536
|
+
personas: Array.isArray(document.metadata.personas) ? structuredClone(document.metadata.personas) : [],
|
|
537
|
+
relationships: Array.isArray(document.metadata.relationships) ? structuredClone(document.metadata.relationships) : [],
|
|
538
|
+
deliveryShape: document.metadata.delivery_shape ? structuredClone(document.metadata.delivery_shape) : null,
|
|
539
|
+
details: parseIntentBodyDetails(document.body),
|
|
540
|
+
},
|
|
541
|
+
state: {
|
|
542
|
+
status: document.metadata.status,
|
|
543
|
+
deliveryStatus: document.metadata.delivery_status,
|
|
544
|
+
currentPhase: document.metadata.current_phase,
|
|
545
|
+
deliveryStatePath: document.metadata.delivery_state_path ?? null,
|
|
546
|
+
},
|
|
547
|
+
destinations: [
|
|
548
|
+
relative(root, path).replaceAll('\\', '/'),
|
|
549
|
+
relative(root, intentStatePath(path)).replaceAll('\\', '/'),
|
|
550
|
+
],
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function listDraftIntentsForReconciliation(projectRoot) {
|
|
555
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
556
|
+
const root = resolve(paths.specsRoot, '2.Purpose/intents');
|
|
557
|
+
const intents = [];
|
|
558
|
+
for (const path of intentMarkdownFiles(root)) {
|
|
559
|
+
try {
|
|
560
|
+
const document = parseIntentDocument(path);
|
|
561
|
+
const reference = `${document.metadata.domain}/${document.metadata.slug}`;
|
|
562
|
+
const candidate = readDraftIntentForReconciliation(paths.projectRoot, reference);
|
|
563
|
+
intents.push({ reference, title: candidate.intent.title, updatedAt: candidate.sourceUpdatedAt });
|
|
564
|
+
} catch {
|
|
565
|
+
// Ineligible or divergent records remain available through governed delivery, not this editor.
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return intents.sort((left, right) => left.title.localeCompare(right.title) || left.reference.localeCompare(right.reference));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function replaceDraftIntent(projectRoot, reference, options = {}) {
|
|
572
|
+
const root = resolve(projectRoot);
|
|
573
|
+
const current = readDraftIntentForReconciliation(root, reference);
|
|
574
|
+
if (!options.expectedUpdatedAt || options.expectedUpdatedAt !== current.sourceUpdatedAt) {
|
|
575
|
+
throw new Error(`Intent cannot be reconciled because a newer canonical revision exists: ${reference}`);
|
|
576
|
+
}
|
|
577
|
+
const proposed = options.intent ?? {};
|
|
578
|
+
if ((proposed.domain && proposed.domain !== current.intent.domain)
|
|
579
|
+
|| (proposed.slug && proposed.slug !== current.intent.slug)) {
|
|
580
|
+
throw new Error('Intent identity cannot change during reconciliation');
|
|
581
|
+
}
|
|
582
|
+
if (proposed.relationships
|
|
583
|
+
&& JSON.stringify(proposed.relationships) !== JSON.stringify(current.intent.relationships)) {
|
|
584
|
+
throw new Error('Intent relationships cannot change during reconciliation');
|
|
585
|
+
}
|
|
586
|
+
const title = String(proposed.title ?? current.intent.title).trim();
|
|
587
|
+
if (!title) throw new Error('Intent title is required');
|
|
588
|
+
const personas = (proposed.personas ?? current.intent.personas).map((persona) => validatedAttachment(persona));
|
|
589
|
+
const deliveryShape = validatedDeliveryShape(proposed.deliveryShape ?? proposed.delivery_shape ?? current.intent.deliveryShape);
|
|
590
|
+
const path = intentSource(root, reference);
|
|
591
|
+
const document = parseIntentDocument(path);
|
|
592
|
+
const metadata = {
|
|
593
|
+
...document.metadata,
|
|
594
|
+
title,
|
|
595
|
+
personas,
|
|
596
|
+
relationships: current.intent.relationships,
|
|
597
|
+
delivery_shape: deliveryShape,
|
|
598
|
+
updated_at: options.now ?? new Date().toISOString(),
|
|
599
|
+
};
|
|
600
|
+
const body = renderIntentBody(title, proposed.details ?? current.intent.details, metadata.relationships, deliveryShape);
|
|
601
|
+
atomicWriteSet(root, [
|
|
602
|
+
{ path, content: `---\n${YAML.stringify(metadata).trimEnd()}\n---\n\n${body}` },
|
|
603
|
+
{ path: intentStatePath(path), content: `${JSON.stringify(structuredIntentState(root, path, metadata), null, 2)}\n` },
|
|
604
|
+
]);
|
|
605
|
+
publishLifecycleEventSafely(root, 'ewai.intent.reconciled', {
|
|
606
|
+
sourceKey: `intent.reconciled:${reference}:${metadata.updated_at}`,
|
|
607
|
+
occurredAt: metadata.updated_at,
|
|
608
|
+
sourceRevision: metadata.updated_at,
|
|
609
|
+
scope: { intent: reference },
|
|
610
|
+
facts: { status: metadata.status, domain: metadata.domain, slug: metadata.slug },
|
|
611
|
+
evidence: [relative(root, path).replaceAll('\\', '/')],
|
|
612
|
+
personas,
|
|
613
|
+
streamId: `intent:${reference}`,
|
|
614
|
+
now: metadata.updated_at,
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
reference,
|
|
618
|
+
markdownPath: relative(root, path).replaceAll('\\', '/'),
|
|
619
|
+
jsonPath: relative(root, intentStatePath(path)).replaceAll('\\', '/'),
|
|
620
|
+
updatedAt: metadata.updated_at,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function intentMarkdownFiles(root, found = []) {
|
|
625
|
+
if (!existsSync(root)) return found;
|
|
626
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
627
|
+
const path = resolve(root, entry.name);
|
|
628
|
+
if (entry.isDirectory()) intentMarkdownFiles(path, found);
|
|
629
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) found.push(path);
|
|
630
|
+
}
|
|
631
|
+
return found;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export function auditIntentStateCopies(projectRoot, options = {}) {
|
|
635
|
+
recoverIntentTransactions(projectRoot);
|
|
636
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
637
|
+
const root = resolve(paths.specsRoot, '2.Purpose/intents');
|
|
638
|
+
const result = { schema: 'ewai.intent-state-audit/v1', checked: 0, created: [], drift: [] };
|
|
639
|
+
for (const path of intentMarkdownFiles(root)) {
|
|
640
|
+
const document = parseIntentDocument(path);
|
|
641
|
+
if (document.metadata.schema !== 'ewai.intent/v1') continue;
|
|
642
|
+
result.checked += 1;
|
|
643
|
+
const sidecar = intentStatePath(path);
|
|
644
|
+
const slug = document.metadata.slug;
|
|
645
|
+
const deliveryPath = resolve(paths.specsRoot, `6.Build/${slug}/delivery-state.json`);
|
|
646
|
+
const delivery = existsSync(deliveryPath) ? JSON.parse(readFileSync(deliveryPath, 'utf8')) : null;
|
|
647
|
+
const expected = delivery ? {
|
|
648
|
+
status: delivery.intent?.status,
|
|
649
|
+
deliveryStatus: delivery.status,
|
|
650
|
+
currentPhase: delivery.currentPhase,
|
|
651
|
+
deliveryStatePath: relative(paths.projectRoot, deliveryPath).replaceAll('\\', '/')
|
|
652
|
+
} : {};
|
|
653
|
+
if (!existsSync(sidecar) && options.repairMissing !== false) {
|
|
654
|
+
const updated = updateIntentDeliveryState(paths.projectRoot, path, {
|
|
655
|
+
status: expected.status ?? document.metadata.status ?? 'draft',
|
|
656
|
+
deliveryStatus: expected.deliveryStatus ?? document.metadata.delivery_status ?? 'not-started',
|
|
657
|
+
currentPhase: expected.currentPhase ?? document.metadata.current_phase ?? 'backlog',
|
|
658
|
+
deliveryStatePath: expected.deliveryStatePath ?? document.metadata.delivery_state_path ?? null
|
|
659
|
+
});
|
|
660
|
+
result.created.push(relative(paths.projectRoot, updated.jsonPath).replaceAll('\\', '/'));
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
validateIntentStateCopies(paths.projectRoot, path, expected);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
result.drift.push({
|
|
667
|
+
intent: relative(paths.projectRoot, path).replaceAll('\\', '/'),
|
|
668
|
+
message: error.message
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
result.status = result.drift.length ? 'drift-detected' : 'consistent';
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function intentSource(projectRoot, reference) {
|
|
677
|
+
const { paths } = loadProjectConfig(projectRoot);
|
|
678
|
+
const parts = String(reference ?? '').split('/');
|
|
679
|
+
const [domain, slug] = parts;
|
|
680
|
+
if (parts.length !== 2 || !slugPattern.test(domain) || !slugPattern.test(slug)) {
|
|
681
|
+
throw new Error(`Intent persona changes require an exact domain/slug reference: ${reference}`);
|
|
682
|
+
}
|
|
683
|
+
const root = resolve(paths.specsRoot, '2.Purpose/intents');
|
|
684
|
+
const path = resolve(root, domain, `${slug}.md`);
|
|
685
|
+
const rel = relative(root, path);
|
|
686
|
+
if (rel.startsWith('..') || !existsSync(path)) throw new Error(`Intent not found: ${reference}`);
|
|
687
|
+
return path;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function updateIntentMetadata(projectRoot, reference, mutate) {
|
|
691
|
+
recoverIntentTransactions(projectRoot);
|
|
692
|
+
const path = intentSource(projectRoot, reference);
|
|
693
|
+
const previous = intentMutationQueues.get(path) ?? Promise.resolve();
|
|
694
|
+
const operation = previous.catch(() => {}).then(() => {
|
|
695
|
+
const document = parseIntentDocument(path);
|
|
696
|
+
const metadata = document.metadata;
|
|
697
|
+
mutate(metadata);
|
|
698
|
+
metadata.updated_at = new Date().toISOString();
|
|
699
|
+
atomicWriteSet(projectRoot, [
|
|
700
|
+
{
|
|
701
|
+
path,
|
|
702
|
+
content: `---\n${YAML.stringify(metadata).trimEnd()}\n---${document.body}`,
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
path: intentStatePath(path),
|
|
706
|
+
content: `${JSON.stringify(structuredIntentState(projectRoot, path, metadata), null, 2)}\n`,
|
|
707
|
+
},
|
|
708
|
+
]);
|
|
709
|
+
return { path, metadata };
|
|
710
|
+
});
|
|
711
|
+
intentMutationQueues.set(path, operation);
|
|
712
|
+
try {
|
|
713
|
+
return await operation;
|
|
714
|
+
} finally {
|
|
715
|
+
if (intentMutationQueues.get(path) === operation) intentMutationQueues.delete(path);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function validatedAttachment(input) {
|
|
720
|
+
const ref = String(input.ref ?? input.personaRef ?? '').trim();
|
|
721
|
+
const role = String(input.role ?? 'reviewer').trim();
|
|
722
|
+
const depth = Number(input.depth ?? 3);
|
|
723
|
+
validatePersonaRef(ref);
|
|
724
|
+
if (!slugPattern.test(role)) throw new Error(`Invalid persona role: ${role}`);
|
|
725
|
+
if (!Number.isInteger(depth) || depth < 1 || depth > 5) throw new Error('Persona depth must be an integer from 1 to 5');
|
|
726
|
+
return { ref, role, depth };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export async function attachPersonaToIntent(projectRoot, reference, input) {
|
|
730
|
+
const attachment = validatedAttachment(input);
|
|
731
|
+
const updated = await updateIntentMetadata(projectRoot, reference, (metadata) => {
|
|
732
|
+
const personas = Array.isArray(metadata.personas) ? metadata.personas : [];
|
|
733
|
+
const existing = personas.findIndex((persona) => persona?.ref === attachment.ref);
|
|
734
|
+
if (existing === -1) personas.push(attachment);
|
|
735
|
+
else personas[existing] = attachment;
|
|
736
|
+
metadata.personas = personas;
|
|
737
|
+
});
|
|
738
|
+
return { reference, attachment, path: updated.path, personas: updated.metadata.personas };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export async function detachPersonaFromIntent(projectRoot, reference, personaRef) {
|
|
742
|
+
const ref = validatedAttachment({ ref: personaRef, role: 'reviewer', depth: 3 }).ref;
|
|
743
|
+
const updated = await updateIntentMetadata(projectRoot, reference, (metadata) => {
|
|
744
|
+
metadata.personas = (Array.isArray(metadata.personas) ? metadata.personas : []).filter((persona) => persona?.ref !== ref);
|
|
745
|
+
});
|
|
746
|
+
return { reference, detached: ref, path: updated.path, personas: updated.metadata.personas };
|
|
747
|
+
}
|