@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,362 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { listKnowledge, readKnowledgeDocument } from './knowledge.mjs';
|
|
5
|
+
import { openRuntimeDatabase } from './database.mjs';
|
|
6
|
+
import { runtimePaths } from './paths.mjs';
|
|
7
|
+
|
|
8
|
+
const markdownExtensions = new Set(['.md', '.markdown']);
|
|
9
|
+
|
|
10
|
+
function hash(value) {
|
|
11
|
+
return createHash('sha256').update(value).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function within(root, target) {
|
|
15
|
+
const path = relative(root, target);
|
|
16
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalized(path) {
|
|
20
|
+
return path.replaceAll('\\', '/');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function titleFromPath(path) {
|
|
24
|
+
return basename(path, extname(path)).replace(/[-_]+/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stripFrontmatter(content) {
|
|
28
|
+
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseSections(document) {
|
|
32
|
+
const content = stripFrontmatter(document.content);
|
|
33
|
+
const lines = content.split(/\r?\n/);
|
|
34
|
+
const headings = [];
|
|
35
|
+
let title = titleFromPath(document.path);
|
|
36
|
+
let hasTitle = false;
|
|
37
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
38
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
|
|
39
|
+
if (!match) continue;
|
|
40
|
+
const level = match[1].length;
|
|
41
|
+
const heading = match[2].replace(/\s+#+\s*$/, '').trim();
|
|
42
|
+
if (level === 1 && !hasTitle) {
|
|
43
|
+
title = heading;
|
|
44
|
+
hasTitle = true;
|
|
45
|
+
}
|
|
46
|
+
headings.push({ index, line: index + 1, level, heading });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!markdownExtensions.has(document.extension) || headings.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
title,
|
|
52
|
+
hasTitle: !markdownExtensions.has(document.extension),
|
|
53
|
+
sections: [{ heading: title, headingPath: title, line: 1, body: content }]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sections = [];
|
|
58
|
+
const stack = [];
|
|
59
|
+
if (headings[0].index > 0 && lines.slice(0, headings[0].index).join('\n').trim()) {
|
|
60
|
+
sections.push({ heading: title, headingPath: title, line: 1, body: lines.slice(0, headings[0].index).join('\n') });
|
|
61
|
+
}
|
|
62
|
+
for (let position = 0; position < headings.length; position += 1) {
|
|
63
|
+
const current = headings[position];
|
|
64
|
+
const end = headings[position + 1]?.index ?? lines.length;
|
|
65
|
+
stack.length = current.level - 1;
|
|
66
|
+
stack[current.level - 1] = current.heading;
|
|
67
|
+
sections.push({
|
|
68
|
+
heading: current.heading,
|
|
69
|
+
headingPath: stack.filter(Boolean).join(' / '),
|
|
70
|
+
line: current.line,
|
|
71
|
+
body: lines.slice(current.index + 1, end).join('\n').trim()
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { title, hasTitle, sections };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function lineFor(content, offset) {
|
|
78
|
+
return content.slice(0, offset).split(/\r?\n/).length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveLink(projectRoot, sourcePath, rawTarget) {
|
|
82
|
+
if (!rawTarget || rawTarget.startsWith('#') || /^(?:https?:|mailto:|tel:|data:)/i.test(rawTarget)) return null;
|
|
83
|
+
const [pathPart, anchor = ''] = rawTarget.replace(/^<|>$/g, '').split('#', 2);
|
|
84
|
+
if (!pathPart) return null;
|
|
85
|
+
let decoded;
|
|
86
|
+
try {
|
|
87
|
+
decoded = decodeURIComponent(pathPart.split('?')[0]);
|
|
88
|
+
} catch {
|
|
89
|
+
decoded = pathPart.split('?')[0];
|
|
90
|
+
}
|
|
91
|
+
const specsRoot = runtimePaths(projectRoot).specsRoot;
|
|
92
|
+
const sourceAbsolute = resolve(specsRoot, sourcePath);
|
|
93
|
+
let absolute;
|
|
94
|
+
if (decoded.startsWith('SPECS/')) absolute = resolve(specsRoot, decoded.slice('SPECS/'.length));
|
|
95
|
+
else if (isAbsolute(decoded)) absolute = resolve(decoded);
|
|
96
|
+
else absolute = resolve(dirname(sourceAbsolute), decoded);
|
|
97
|
+
|
|
98
|
+
if (within(specsRoot, absolute)) {
|
|
99
|
+
return { kind: 'specs', targetPath: normalized(relative(specsRoot, absolute)), anchor, exists: existsSync(absolute) };
|
|
100
|
+
}
|
|
101
|
+
if (within(resolve(projectRoot), absolute)) {
|
|
102
|
+
return { kind: 'project', targetPath: normalized(relative(projectRoot, absolute)), anchor, exists: existsSync(absolute) };
|
|
103
|
+
}
|
|
104
|
+
return { kind: 'absolute-local', targetPath: absolute, anchor, exists: existsSync(absolute) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseLinks(projectRoot, document) {
|
|
108
|
+
if (!markdownExtensions.has(document.extension)) return [];
|
|
109
|
+
const links = [];
|
|
110
|
+
const markdownLink = /!?\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
111
|
+
for (const match of document.content.matchAll(markdownLink)) {
|
|
112
|
+
const resolved = resolveLink(projectRoot, document.path, match[1]);
|
|
113
|
+
if (!resolved) continue;
|
|
114
|
+
links.push({ ...resolved, sourcePath: document.path, rawTarget: match[1], line: lineFor(document.content, match.index) });
|
|
115
|
+
}
|
|
116
|
+
const codeReference = /`((?:SPECS|src|app|tests?|public|config|docs)\/[A-Za-z0-9_.@/\-[\]]+(?::\d+)?)`/g;
|
|
117
|
+
for (const match of document.content.matchAll(codeReference)) {
|
|
118
|
+
const rawTarget = match[1].replace(/:\d+$/, '');
|
|
119
|
+
if (links.some((link) => link.rawTarget === rawTarget && link.line === lineFor(document.content, match.index))) continue;
|
|
120
|
+
const projectTarget = rawTarget.startsWith('SPECS/')
|
|
121
|
+
? resolveLink(projectRoot, document.path, rawTarget)
|
|
122
|
+
: { kind: 'project', targetPath: rawTarget, anchor: '', exists: existsSync(resolve(projectRoot, rawTarget)) };
|
|
123
|
+
if (!projectTarget) continue;
|
|
124
|
+
links.push({ ...projectTarget, sourcePath: document.path, rawTarget, line: lineFor(document.content, match.index) });
|
|
125
|
+
}
|
|
126
|
+
return links;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function inventory(projectRoot) {
|
|
130
|
+
const documents = listKnowledge(projectRoot).documents
|
|
131
|
+
.map((entry) => readKnowledgeDocument(projectRoot, entry.path))
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.map((document) => ({ ...document, contentHash: hash(document.content) }));
|
|
134
|
+
const fingerprint = hash(JSON.stringify(documents.map((document) => [document.path, document.contentHash])));
|
|
135
|
+
return { documents, fingerprint };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function currentFingerprint(database) {
|
|
139
|
+
return database.prepare("SELECT value FROM runtime_meta WHERE key = 'palace_source_fingerprint'").get()?.value ?? '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function refreshPalaceIndex(projectRoot, options = {}) {
|
|
143
|
+
const source = inventory(projectRoot);
|
|
144
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
145
|
+
try {
|
|
146
|
+
const indexedDocuments = database.prepare('SELECT COUNT(*) AS count FROM palace_documents').get().count;
|
|
147
|
+
if (!options.force && currentFingerprint(database) === source.fingerprint && indexedDocuments === source.documents.length) {
|
|
148
|
+
const latest = database.prepare("SELECT * FROM palace_index_runs WHERE status = 'completed' ORDER BY id DESC LIMIT 1").get();
|
|
149
|
+
return {
|
|
150
|
+
schema: 'ewai.palace-index/v1',
|
|
151
|
+
status: 'current',
|
|
152
|
+
runId: latest?.id ?? null,
|
|
153
|
+
documents: indexedDocuments,
|
|
154
|
+
sections: latest?.section_count ?? 0,
|
|
155
|
+
links: latest?.link_count ?? 0,
|
|
156
|
+
fingerprint: source.fingerprint
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const run = database.prepare(`
|
|
161
|
+
INSERT INTO palace_index_runs (status, source_fingerprint)
|
|
162
|
+
VALUES ('running', ?) RETURNING id
|
|
163
|
+
`).get(source.fingerprint);
|
|
164
|
+
let sectionCount = 0;
|
|
165
|
+
let linkCount = 0;
|
|
166
|
+
database.exec('BEGIN IMMEDIATE');
|
|
167
|
+
try {
|
|
168
|
+
database.exec('DELETE FROM palace_sections_fts; DELETE FROM palace_links; DELETE FROM palace_documents;');
|
|
169
|
+
const insertDocument = database.prepare(`
|
|
170
|
+
INSERT INTO palace_documents (
|
|
171
|
+
path, title, extension, content_hash, content_length, source_mtime, has_title, section_count
|
|
172
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
173
|
+
`);
|
|
174
|
+
const insertSection = database.prepare(`
|
|
175
|
+
INSERT INTO palace_sections_fts (path, title, heading, body, heading_path, line)
|
|
176
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
177
|
+
`);
|
|
178
|
+
const insertLink = database.prepare(`
|
|
179
|
+
INSERT INTO palace_links (
|
|
180
|
+
source_path, target_path, target_anchor, kind, exists_on_disk, line, raw_target
|
|
181
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
182
|
+
`);
|
|
183
|
+
for (const document of source.documents) {
|
|
184
|
+
const parsed = parseSections(document);
|
|
185
|
+
const links = parseLinks(projectRoot, document);
|
|
186
|
+
insertDocument.run(
|
|
187
|
+
document.path, parsed.title, document.extension, document.contentHash,
|
|
188
|
+
document.content.length, document.updatedAt, parsed.hasTitle ? 1 : 0, parsed.sections.length
|
|
189
|
+
);
|
|
190
|
+
for (const section of parsed.sections) {
|
|
191
|
+
insertSection.run(document.path, parsed.title, section.heading, section.body, section.headingPath, section.line);
|
|
192
|
+
sectionCount += 1;
|
|
193
|
+
}
|
|
194
|
+
for (const link of links) {
|
|
195
|
+
insertLink.run(
|
|
196
|
+
link.sourcePath, link.targetPath, link.anchor, link.kind,
|
|
197
|
+
link.exists ? 1 : 0, link.line, link.rawTarget
|
|
198
|
+
);
|
|
199
|
+
linkCount += 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
database.prepare(`
|
|
203
|
+
INSERT INTO runtime_meta (key, value, updated_at)
|
|
204
|
+
VALUES ('palace_source_fingerprint', ?, CURRENT_TIMESTAMP)
|
|
205
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
|
206
|
+
`).run(source.fingerprint);
|
|
207
|
+
database.prepare(`
|
|
208
|
+
UPDATE palace_index_runs SET
|
|
209
|
+
status = 'completed', document_count = ?, section_count = ?, link_count = ?,
|
|
210
|
+
completed_at = CURRENT_TIMESTAMP, summary = ?
|
|
211
|
+
WHERE id = ?
|
|
212
|
+
`).run(source.documents.length, sectionCount, linkCount, 'SPECS Mind Palace index refreshed.', run.id);
|
|
213
|
+
database.exec('COMMIT');
|
|
214
|
+
} catch (error) {
|
|
215
|
+
database.exec('ROLLBACK');
|
|
216
|
+
database.prepare(`UPDATE palace_index_runs SET status = 'failed', completed_at = CURRENT_TIMESTAMP, summary = ? WHERE id = ?`)
|
|
217
|
+
.run(error.message, run.id);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
schema: 'ewai.palace-index/v1', status: 'completed', runId: run.id,
|
|
222
|
+
documents: source.documents.length, sections: sectionCount, links: linkCount,
|
|
223
|
+
fingerprint: source.fingerprint
|
|
224
|
+
};
|
|
225
|
+
} finally {
|
|
226
|
+
database.close();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function palaceIndexStatus(projectRoot) {
|
|
231
|
+
const source = inventory(projectRoot);
|
|
232
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
233
|
+
try {
|
|
234
|
+
const latest = database.prepare("SELECT * FROM palace_index_runs WHERE status = 'completed' ORDER BY id DESC LIMIT 1").get();
|
|
235
|
+
const counts = database.prepare('SELECT COUNT(*) AS documents, COALESCE(SUM(section_count), 0) AS sections FROM palace_documents').get();
|
|
236
|
+
if (!latest) return { schema: 'ewai.palace-index-status/v1', status: 'missing', documents: 0, sections: 0 };
|
|
237
|
+
return {
|
|
238
|
+
schema: 'ewai.palace-index-status/v1',
|
|
239
|
+
status: currentFingerprint(database) === source.fingerprint ? 'fresh' : 'stale',
|
|
240
|
+
runId: latest.id,
|
|
241
|
+
documents: counts.documents,
|
|
242
|
+
sections: counts.sections,
|
|
243
|
+
indexedAt: latest.completed_at
|
|
244
|
+
};
|
|
245
|
+
} finally {
|
|
246
|
+
database.close();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function ftsQuery(query) {
|
|
251
|
+
const terms = String(query).normalize('NFKC').match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
|
252
|
+
return terms.map((term) => `"${term.replaceAll('"', '""')}"*`).join(' OR ');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function searchPalace(projectRoot, query, options = {}) {
|
|
256
|
+
const index = refreshPalaceIndex(projectRoot);
|
|
257
|
+
const expression = ftsQuery(query);
|
|
258
|
+
if (!expression) return { schema: 'ewai.palace-search/v1', query, index, results: [] };
|
|
259
|
+
const requestedLimit = Number(options.limit ?? 50);
|
|
260
|
+
const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? Math.floor(requestedLimit) : 50;
|
|
261
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
262
|
+
try {
|
|
263
|
+
const rows = database.prepare(`
|
|
264
|
+
SELECT path, title, heading, heading_path, CAST(line AS INTEGER) AS line,
|
|
265
|
+
snippet(palace_sections_fts, 3, '⟦', '⟧', ' … ', 24) AS snippet,
|
|
266
|
+
bm25(palace_sections_fts, 4.0, 3.0, 2.0, 1.0) AS rank
|
|
267
|
+
FROM palace_sections_fts
|
|
268
|
+
WHERE palace_sections_fts MATCH ?
|
|
269
|
+
ORDER BY rank, path, CAST(line AS INTEGER)
|
|
270
|
+
LIMIT ?
|
|
271
|
+
`).all(expression, limit * 3);
|
|
272
|
+
const seen = new Set();
|
|
273
|
+
const results = [];
|
|
274
|
+
for (const row of rows) {
|
|
275
|
+
if (seen.has(row.path)) continue;
|
|
276
|
+
seen.add(row.path);
|
|
277
|
+
results.push(row);
|
|
278
|
+
if (results.length >= limit) break;
|
|
279
|
+
}
|
|
280
|
+
return { schema: 'ewai.palace-search/v1', query, index, results };
|
|
281
|
+
} finally {
|
|
282
|
+
database.close();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function finding(code, severity, path, message, details = {}) {
|
|
287
|
+
return { code, severity, path, message, details };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function palaceTidiness(projectRoot) {
|
|
291
|
+
const index = refreshPalaceIndex(projectRoot);
|
|
292
|
+
const database = openRuntimeDatabase(projectRoot);
|
|
293
|
+
try {
|
|
294
|
+
const documents = database.prepare('SELECT * FROM palace_documents ORDER BY path').all();
|
|
295
|
+
const findings = [];
|
|
296
|
+
for (const document of documents) {
|
|
297
|
+
if (document.content_length === 0) findings.push(finding('empty-document', 'warning', document.path, 'The document is empty.'));
|
|
298
|
+
if (markdownExtensions.has(document.extension) && !document.has_title) {
|
|
299
|
+
findings.push(finding('missing-title', 'warning', document.path, 'The Markdown document has no level-one title.'));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const link of database.prepare(`SELECT * FROM palace_links WHERE exists_on_disk = 0 ORDER BY source_path, line`).all()) {
|
|
303
|
+
findings.push(finding(
|
|
304
|
+
'broken-internal-link', 'warning', link.source_path,
|
|
305
|
+
`The reference ${link.raw_target} does not resolve on disk.`,
|
|
306
|
+
{ target: link.target_path, kind: link.kind, line: link.line }
|
|
307
|
+
));
|
|
308
|
+
}
|
|
309
|
+
for (const group of database.prepare(`
|
|
310
|
+
SELECT content_hash, GROUP_CONCAT(path, '||') AS paths, COUNT(*) AS count
|
|
311
|
+
FROM palace_documents WHERE content_length > 0
|
|
312
|
+
GROUP BY content_hash HAVING COUNT(*) > 1
|
|
313
|
+
`).all()) {
|
|
314
|
+
const paths = group.paths.split('||');
|
|
315
|
+
for (const path of paths) findings.push(finding('duplicate-content', 'notice', path, 'Another Palace document has identical content.', { matches: paths.filter((candidate) => candidate !== path) }));
|
|
316
|
+
}
|
|
317
|
+
const linked = new Set();
|
|
318
|
+
for (const link of database.prepare("SELECT source_path, target_path FROM palace_links WHERE kind = 'specs' AND exists_on_disk = 1").all()) {
|
|
319
|
+
linked.add(link.source_path);
|
|
320
|
+
linked.add(link.target_path);
|
|
321
|
+
}
|
|
322
|
+
for (const document of documents) {
|
|
323
|
+
if (!markdownExtensions.has(document.extension) || linked.has(document.path)) continue;
|
|
324
|
+
if (/(?:^|\/)(?:README|_template)\.md$/i.test(document.path)) continue;
|
|
325
|
+
findings.push(finding('orphaned-document', 'notice', document.path, 'No other indexed SPECS document links to or from this document.'));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const counts = { warning: 0, notice: 0 };
|
|
329
|
+
for (const item of findings) counts[item.severity] = (counts[item.severity] ?? 0) + 1;
|
|
330
|
+
return {
|
|
331
|
+
schema: 'ewai.palace-tidiness/v1',
|
|
332
|
+
status: counts.warning > 0 || counts.notice > 0 ? 'housekeeping-recommended' : 'tidy',
|
|
333
|
+
documents: documents.length,
|
|
334
|
+
sections: index.sections,
|
|
335
|
+
links: index.links,
|
|
336
|
+
counts,
|
|
337
|
+
findings
|
|
338
|
+
};
|
|
339
|
+
} finally {
|
|
340
|
+
database.close();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function palaceHousekeeping(projectRoot) {
|
|
345
|
+
const tidiness = palaceTidiness(projectRoot);
|
|
346
|
+
const grouped = new Map();
|
|
347
|
+
for (const item of tidiness.findings) {
|
|
348
|
+
if (!grouped.has(item.code)) grouped.set(item.code, []);
|
|
349
|
+
grouped.get(item.code).push(item);
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
schema: 'ewai.palace-housekeeping/v1',
|
|
353
|
+
status: tidiness.status,
|
|
354
|
+
requiresApproval: true,
|
|
355
|
+
changedFiles: 0,
|
|
356
|
+
groups: [...grouped.entries()].map(([code, findings]) => ({ code, count: findings.length, findings })),
|
|
357
|
+
guidance: tidiness.status === 'tidy'
|
|
358
|
+
? 'The Mind Palace is tidy. A no-op is the correct housekeeping result.'
|
|
359
|
+
: 'Use $ewai-palace-housekeeping to review these findings with the user before changing canonical SPECS.',
|
|
360
|
+
tidiness
|
|
361
|
+
};
|
|
362
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { projectPaths } from '../paths.mjs';
|
|
4
|
+
|
|
5
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export function runtimePaths(projectRoot) {
|
|
8
|
+
const project = projectPaths(projectRoot);
|
|
9
|
+
const root = project.projectRoot;
|
|
10
|
+
const runtimeRoot = project.runtimeRoot;
|
|
11
|
+
return {
|
|
12
|
+
projectRoot: root,
|
|
13
|
+
runtimeRoot,
|
|
14
|
+
dataRoot: resolve(runtimeRoot, 'data'),
|
|
15
|
+
databasePath: resolve(runtimeRoot, 'data/pipeline.sqlite'),
|
|
16
|
+
runtimeStateRoot: resolve(runtimeRoot, 'runtime'),
|
|
17
|
+
dashboardStatePath: resolve(runtimeRoot, 'runtime/dashboard.json'),
|
|
18
|
+
checkinStatePath: resolve(runtimeRoot, 'runtime/checkin.json'),
|
|
19
|
+
guidedDiscoveryDraftPath: resolve(runtimeRoot, 'runtime/guided-discovery.json'),
|
|
20
|
+
guidedIntentDraftPath: resolve(runtimeRoot, 'runtime/guided-intent.json'),
|
|
21
|
+
evidenceDepthRoot: resolve(runtimeRoot, 'runtime/evidence-depth'),
|
|
22
|
+
evidenceDepthPreparationPath: resolve(runtimeRoot, 'runtime/evidence-depth/preparation.json'),
|
|
23
|
+
prototypeIterationsRoot: resolve(runtimeRoot, 'runtime/prototype-iterations'),
|
|
24
|
+
prototypeIterationRoot: (slug) => resolve(runtimeRoot, 'runtime/prototype-iterations', slug),
|
|
25
|
+
prototypePlanPreparationPath: (slug) => resolve(runtimeRoot, 'runtime/prototype-iterations', slug, 'plan-preparation.json'),
|
|
26
|
+
prototypeCyclePreparationPath: (slug, cycle) => resolve(runtimeRoot, 'runtime/prototype-iterations', slug, `cycle-${cycle}-preparation.json`),
|
|
27
|
+
phaseContributionsRoot: resolve(runtimeRoot, 'runtime/phase-contributions'),
|
|
28
|
+
phaseContributionDraftPath: (slug, phase) => resolve(runtimeRoot, 'runtime/phase-contributions', slug, `${phase}.json`),
|
|
29
|
+
contextPacksRoot: resolve(runtimeRoot, 'runtime/context-packs'),
|
|
30
|
+
contextPackManifestRoot: resolve(runtimeRoot, 'runtime/context-packs/manifests'),
|
|
31
|
+
contextPackCacheRoot: resolve(runtimeRoot, 'runtime/context-packs/cache'),
|
|
32
|
+
starterMaterialisationRoot: resolve(runtimeRoot, 'starter-materialisation'),
|
|
33
|
+
starterStagingRoot: resolve(runtimeRoot, 'starter-materialisation/staging'),
|
|
34
|
+
errorReportsRoot: resolve(runtimeRoot, 'error-reports'),
|
|
35
|
+
errorReportDraftsRoot: resolve(runtimeRoot, 'error-reports/drafts'),
|
|
36
|
+
errorReportPackagesRoot: resolve(runtimeRoot, 'error-reports/packages'),
|
|
37
|
+
errorReportReceiptsRoot: resolve(runtimeRoot, 'error-reports/receipts'),
|
|
38
|
+
errorReportArchiveRoot: resolve(runtimeRoot, 'error-reports/archive'),
|
|
39
|
+
errorReportAdaptersRoot: resolve(runtimeRoot, 'error-reports/adapters'),
|
|
40
|
+
errorReportSettingsPath: resolve(runtimeRoot, 'error-reports/settings.json'),
|
|
41
|
+
teamHubRoot: resolve(runtimeRoot, 'team-hub'),
|
|
42
|
+
teamHubConnectionPath: resolve(runtimeRoot, 'team-hub/connection.json'),
|
|
43
|
+
teamHubAttemptsRoot: resolve(runtimeRoot, 'team-hub/attempts'),
|
|
44
|
+
teamHubAcceptedReceiptPath: resolve(runtimeRoot, 'team-hub/accepted-receipt.json'),
|
|
45
|
+
teamHubResourcesRoot: resolve(runtimeRoot, 'team-hub/resources'),
|
|
46
|
+
teamHubResourceCacheRoot: resolve(runtimeRoot, 'team-hub/resources/cache'),
|
|
47
|
+
teamHubResourceStagingRoot: resolve(runtimeRoot, 'team-hub/resources/staging'),
|
|
48
|
+
teamHubResourceReceiptsRoot: resolve(runtimeRoot, 'team-hub/resources/receipts'),
|
|
49
|
+
teamHubResourceInstalledPath: resolve(runtimeRoot, 'team-hub/resources/installed.json'),
|
|
50
|
+
teamHubManagedPackRoot: resolve(runtimeRoot, 'packs/team-hub'),
|
|
51
|
+
logRoot: resolve(runtimeRoot, 'logs'),
|
|
52
|
+
dashboardLogPath: resolve(runtimeRoot, 'logs/dashboard.log'),
|
|
53
|
+
specsRoot: project.specsRoot,
|
|
54
|
+
specsRelative: project.specsRelative,
|
|
55
|
+
intentsRoot: resolve(project.specsRoot, '2.Purpose/intents'),
|
|
56
|
+
publicRoot: resolve(moduleDir, '../../public')
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const tierRanks = { project: 4, premium: 3, personal: 2, core: 1 };
|
|
4
|
+
|
|
5
|
+
function cleanText(value) {
|
|
6
|
+
return String(value ?? '').trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function words(value) {
|
|
10
|
+
return cleanText(value)
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.split(/[^a-z0-9]+/)
|
|
13
|
+
.filter((word) => word.length >= 4);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function personaHaystack(persona) {
|
|
17
|
+
return [persona.id, persona.name, persona.category, persona.description, ...(persona.tags ?? []), ...(persona.capabilities ?? [])]
|
|
18
|
+
.map((value) => cleanText(value).toLowerCase())
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(' ');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function engagementReason(persona, contextLabel, matches) {
|
|
24
|
+
const concerns = matches.slice(0, 2).join(' and ');
|
|
25
|
+
return `${persona.name} is engaged to examine ${cleanText(contextLabel).toLowerCase()} through ${concerns || 'its relevant project lens'}.`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function selectContextualPersonas(options = {}) {
|
|
29
|
+
const signals = [...new Set((options.signals ?? []).map(cleanText).filter(Boolean))];
|
|
30
|
+
const contextText = JSON.stringify(options.context ?? {}).toLowerCase();
|
|
31
|
+
const catalogue = Array.isArray(options.personaCatalogue) ? options.personaCatalogue : [];
|
|
32
|
+
const limit = Math.max(1, Math.min(8, Number(options.limit ?? 4)));
|
|
33
|
+
const ranked = catalogue.map((persona) => {
|
|
34
|
+
const haystack = personaHaystack(persona);
|
|
35
|
+
const matchedSignals = [];
|
|
36
|
+
let score = 0;
|
|
37
|
+
for (const signal of signals) {
|
|
38
|
+
const normalised = signal.toLowerCase();
|
|
39
|
+
const tokenMatches = words(normalised).filter((word) => haystack.includes(word));
|
|
40
|
+
if (haystack.includes(normalised) || tokenMatches.length) {
|
|
41
|
+
matchedSignals.push(signal);
|
|
42
|
+
score += haystack.includes(normalised) ? 6 : tokenMatches.length * 2;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const tag of [...(persona.tags ?? []), ...(persona.capabilities ?? [])]) {
|
|
46
|
+
if (cleanText(tag).length >= 4 && contextText.includes(cleanText(tag).toLowerCase())) score += 1;
|
|
47
|
+
}
|
|
48
|
+
return { persona, score, matchedSignals };
|
|
49
|
+
}).filter((candidate) => candidate.score > 0)
|
|
50
|
+
.sort((left, right) => (
|
|
51
|
+
right.score - left.score
|
|
52
|
+
|| (tierRanks[right.persona.tier] ?? 0) - (tierRanks[left.persona.tier] ?? 0)
|
|
53
|
+
|| cleanText(left.persona.name).localeCompare(cleanText(right.persona.name))
|
|
54
|
+
|| cleanText(left.persona.id).localeCompare(cleanText(right.persona.id))
|
|
55
|
+
));
|
|
56
|
+
|
|
57
|
+
const selected = [];
|
|
58
|
+
for (const tier of ['project', 'premium']) {
|
|
59
|
+
const candidate = ranked.find((item) => item.persona.tier === tier);
|
|
60
|
+
if (candidate && !selected.includes(candidate)) selected.push(candidate);
|
|
61
|
+
}
|
|
62
|
+
for (const candidate of ranked) {
|
|
63
|
+
if (selected.length >= limit) break;
|
|
64
|
+
if (!selected.includes(candidate)) selected.push(candidate);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return selected.slice(0, limit).map(({ persona, matchedSignals }) => ({
|
|
68
|
+
id: cleanText(persona.id),
|
|
69
|
+
name: cleanText(persona.name),
|
|
70
|
+
tier: cleanText(persona.tier) || 'core',
|
|
71
|
+
category: cleanText(persona.category),
|
|
72
|
+
description: cleanText(persona.description).slice(0, 240),
|
|
73
|
+
matchedSignals,
|
|
74
|
+
engagementReason: engagementReason(persona, options.contextLabel || 'this context', matchedSignals)
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const policyStageSignals = Object.freeze({
|
|
79
|
+
facts: ['policy', 'evidence', 'data', 'privacy', 'security', 'design facts'],
|
|
80
|
+
evaluation: ['policy', 'evidence', 'security', 'destination', 'environment', 'controls'],
|
|
81
|
+
review: ['policy review', 'decision', 'evidence', 'security', 'risk'],
|
|
82
|
+
exception: ['exception', 'scope', 'expiry', 'ownership', 'compensating controls', 'risk'],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export function selectPolicyPersonas(context = {}, personaCatalogue = []) {
|
|
86
|
+
const stage = cleanText(context.stage || 'facts').toLowerCase();
|
|
87
|
+
const dimensions = (context.dimensions ?? []).map((dimension) => cleanText(dimension).replaceAll('_', '-')).filter(Boolean);
|
|
88
|
+
const signals = [...new Set([...(policyStageSignals[stage] ?? policyStageSignals.facts), ...dimensions])];
|
|
89
|
+
const activePersonas = selectContextualPersonas({
|
|
90
|
+
contextLabel: `${stage} policy design work`,
|
|
91
|
+
signals,
|
|
92
|
+
context: context.context ?? {},
|
|
93
|
+
personaCatalogue,
|
|
94
|
+
limit: 4,
|
|
95
|
+
});
|
|
96
|
+
const premiumInstalled = personaCatalogue.some((persona) => cleanText(persona.tier) === 'premium');
|
|
97
|
+
return {
|
|
98
|
+
schema: 'ewai.policy-persona-engagement/v1',
|
|
99
|
+
stage,
|
|
100
|
+
activePersonas,
|
|
101
|
+
baseline: {
|
|
102
|
+
standardModelAvailable: true,
|
|
103
|
+
completeWithoutPremium: true,
|
|
104
|
+
},
|
|
105
|
+
premium: {
|
|
106
|
+
installed: premiumInstalled,
|
|
107
|
+
reason: premiumInstalled
|
|
108
|
+
? 'Installed premium personas may add relevant challenge depth.'
|
|
109
|
+
: 'The premium persona library is not installed; core policy behaviour remains available.',
|
|
110
|
+
},
|
|
111
|
+
authority: {
|
|
112
|
+
advisory: true,
|
|
113
|
+
mayConfirmFacts: false,
|
|
114
|
+
mayApproveReviews: false,
|
|
115
|
+
mayApproveExceptions: false,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const evidenceDepthSignals = Object.freeze({
|
|
121
|
+
architecture: ['architecture', 'dependency', 'integration', 'evidence', 'archaeology'],
|
|
122
|
+
data: ['data', 'privacy', 'retention', 'classification', 'evidence'],
|
|
123
|
+
security: ['security', 'trust boundary', 'identity', 'privacy', 'evidence'],
|
|
124
|
+
product: ['product', 'user journey', 'outcomes', 'acceptance', 'evidence'],
|
|
125
|
+
delivery: ['delivery', 'testing', 'release', 'quality', 'evidence'],
|
|
126
|
+
governance: ['governance', 'policy', 'compliance', 'decision', 'evidence'],
|
|
127
|
+
operations: ['operations', 'hosting', 'recovery', 'observability', 'evidence'],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export function selectEvidenceDepthPersonas(context = {}, personaCatalogue = []) {
|
|
131
|
+
const dimension = cleanText(context.dimension || 'architecture').toLowerCase();
|
|
132
|
+
const gapConditions = (context.gapConditions ?? []).map(cleanText).filter(Boolean);
|
|
133
|
+
const signals = [...new Set([...(evidenceDepthSignals[dimension] ?? evidenceDepthSignals.architecture), ...gapConditions])];
|
|
134
|
+
const activePersonas = selectContextualPersonas({
|
|
135
|
+
contextLabel: `${dimension} evidence-depth investigation`,
|
|
136
|
+
signals,
|
|
137
|
+
context: context.context ?? {},
|
|
138
|
+
personaCatalogue,
|
|
139
|
+
limit: 4,
|
|
140
|
+
});
|
|
141
|
+
const premiumInstalled = personaCatalogue.some((persona) => cleanText(persona.tier) === 'premium');
|
|
142
|
+
return {
|
|
143
|
+
schema: 'ewai.evidence-depth-persona-engagement/v1',
|
|
144
|
+
dimension,
|
|
145
|
+
activePersonas,
|
|
146
|
+
baseline: {
|
|
147
|
+
standardModelAvailable: true,
|
|
148
|
+
completeWithoutPremium: true,
|
|
149
|
+
},
|
|
150
|
+
premium: {
|
|
151
|
+
installed: premiumInstalled,
|
|
152
|
+
reason: premiumInstalled
|
|
153
|
+
? 'Installed premium personas may add relevant specialist challenge depth.'
|
|
154
|
+
: 'The premium persona library is not installed; core and project persona capability remains complete.',
|
|
155
|
+
},
|
|
156
|
+
authority: {
|
|
157
|
+
advisory: true,
|
|
158
|
+
mayDeclareOwnerEvidence: false,
|
|
159
|
+
maySelectDepth: false,
|
|
160
|
+
mayApproveGrouping: false,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const prototypeStageSignals = Object.freeze({
|
|
166
|
+
plan: [
|
|
167
|
+
'product outcomes', 'user journey', 'acceptance', 'information architecture',
|
|
168
|
+
'decision ownership', 'accessibility', 'prototype plan',
|
|
169
|
+
],
|
|
170
|
+
design: [
|
|
171
|
+
'rendered viewport', 'visual hierarchy', 'interaction', 'responsive',
|
|
172
|
+
'accessibility', 'usability', 'prototype critique',
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function personaAvailability(personaCatalogue) {
|
|
177
|
+
const counts = Object.fromEntries(['core', 'project', 'personal', 'premium'].map((tier) => [
|
|
178
|
+
tier,
|
|
179
|
+
personaCatalogue.filter((persona) => cleanText(persona.tier || 'core') === tier).length,
|
|
180
|
+
]));
|
|
181
|
+
return {
|
|
182
|
+
standardModel: { available: true },
|
|
183
|
+
core: { available: counts.core > 0, count: counts.core },
|
|
184
|
+
project: { available: counts.project > 0, count: counts.project },
|
|
185
|
+
personal: { available: counts.personal > 0, count: counts.personal },
|
|
186
|
+
premium: { available: counts.premium > 0, count: counts.premium },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function prototypeFallback(personaCatalogue, stage, limit) {
|
|
191
|
+
const preferredIds = stage === 'plan'
|
|
192
|
+
? ['project.product-owner', 'ewai.core.end-user']
|
|
193
|
+
: ['ewai.core.end-user', 'project.product-owner'];
|
|
194
|
+
const ranked = [...personaCatalogue].sort((left, right) => {
|
|
195
|
+
const leftPreferred = preferredIds.indexOf(cleanText(left.id));
|
|
196
|
+
const rightPreferred = preferredIds.indexOf(cleanText(right.id));
|
|
197
|
+
const leftRank = leftPreferred === -1 ? 100 : leftPreferred;
|
|
198
|
+
const rightRank = rightPreferred === -1 ? 100 : rightPreferred;
|
|
199
|
+
return leftRank - rightRank
|
|
200
|
+
|| (tierRanks[right.tier] ?? 0) - (tierRanks[left.tier] ?? 0)
|
|
201
|
+
|| cleanText(left.name).localeCompare(cleanText(right.name));
|
|
202
|
+
});
|
|
203
|
+
const baseline = ranked.filter((persona) => ['project', 'core'].includes(cleanText(persona.tier || 'core'))).slice(0, limit);
|
|
204
|
+
return baseline.map((persona) => ({
|
|
205
|
+
id: cleanText(persona.id),
|
|
206
|
+
name: cleanText(persona.name),
|
|
207
|
+
tier: cleanText(persona.tier) || 'core',
|
|
208
|
+
category: cleanText(persona.category),
|
|
209
|
+
description: cleanText(persona.description).slice(0, 240),
|
|
210
|
+
matchedSignals: ['baseline prototype review'],
|
|
211
|
+
engagementReason: `${cleanText(persona.name)} is engaged as a baseline ${stage} review perspective because no stronger catalogue match was found.`,
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function selectPrototypeReviewPersonas(context = {}, personaCatalogue = []) {
|
|
216
|
+
const stage = cleanText(context.stage || 'plan').toLowerCase();
|
|
217
|
+
if (!['plan', 'design'].includes(stage)) throw new Error('Prototype persona engagement stage must be plan or design');
|
|
218
|
+
const catalogue = Array.isArray(personaCatalogue)
|
|
219
|
+
? personaCatalogue.map((persona) => ({ ...persona, tier: cleanText(persona.tier) || 'core' }))
|
|
220
|
+
: [];
|
|
221
|
+
const suppliedSignals = (context.signals ?? []).map(cleanText).filter(Boolean);
|
|
222
|
+
const signals = [...new Set(suppliedSignals.length ? suppliedSignals : (prototypeStageSignals[stage] ?? []))];
|
|
223
|
+
const limit = Math.max(1, Math.min(8, Number(context.limit ?? 6)));
|
|
224
|
+
let activePersonas = selectContextualPersonas({
|
|
225
|
+
contextLabel: stage === 'plan' ? 'prototype plan review' : 'rendered prototype review',
|
|
226
|
+
signals,
|
|
227
|
+
context: context.context ?? {},
|
|
228
|
+
personaCatalogue: catalogue,
|
|
229
|
+
limit,
|
|
230
|
+
});
|
|
231
|
+
if (!activePersonas.length) activePersonas = prototypeFallback(catalogue, stage, limit);
|
|
232
|
+
const availability = personaAvailability(catalogue);
|
|
233
|
+
const fingerprintInput = {
|
|
234
|
+
stage,
|
|
235
|
+
signals: [...signals].sort(),
|
|
236
|
+
activePersonas: activePersonas.map(({ id, tier, matchedSignals }) => ({ id, tier, matchedSignals: [...matchedSignals].sort() })).sort((left, right) => left.id.localeCompare(right.id)),
|
|
237
|
+
availability,
|
|
238
|
+
};
|
|
239
|
+
const premiumNotice = availability.premium.available
|
|
240
|
+
? 'Installed premium personas are eligible when their specialism matches this stage.'
|
|
241
|
+
: 'Premium personas are not installed; standard model, core, and project persona review remains complete.';
|
|
242
|
+
const personalNotice = availability.personal.available
|
|
243
|
+
? 'Installed personal personas are eligible when their local perspective matches this stage.'
|
|
244
|
+
: 'Personal personas are not installed and are optional enrichment.';
|
|
245
|
+
return {
|
|
246
|
+
schema: 'ewai.prototype-persona-engagement/v1',
|
|
247
|
+
stage,
|
|
248
|
+
selectionFingerprint: `sha256:${createHash('sha256').update(JSON.stringify(fingerprintInput)).digest('hex')}`,
|
|
249
|
+
activePersonas,
|
|
250
|
+
availability,
|
|
251
|
+
baseline: { completeWithoutOptionalPersonas: true },
|
|
252
|
+
notices: [premiumNotice, personalNotice],
|
|
253
|
+
authority: { advisory: true, maySelectPrototype: false, mayApproveManualQa: false },
|
|
254
|
+
};
|
|
255
|
+
}
|