@topogram/cli 0.3.34
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/ARCHITECTURE.md +67 -0
- package/CHANGELOG.md +240 -0
- package/README.md +223 -0
- package/package.json +51 -0
- package/src/adoption/index.js +3 -0
- package/src/adoption/plan.js +702 -0
- package/src/adoption/reporting.js +464 -0
- package/src/adoption/review-groups.js +313 -0
- package/src/agent-ops/query-builders.js +5012 -0
- package/src/archive/archive.js +141 -0
- package/src/archive/compact.js +26 -0
- package/src/archive/jsonl.js +70 -0
- package/src/archive/resolver-bridge.js +82 -0
- package/src/archive/schema.js +87 -0
- package/src/archive/unarchive.js +108 -0
- package/src/catalog.js +752 -0
- package/src/cli/catalog-alias.js +166 -0
- package/src/cli.js +9738 -0
- package/src/component-behavior.js +173 -0
- package/src/example-implementation.js +91 -0
- package/src/format.js +19 -0
- package/src/generator/adapters.d.ts +4 -0
- package/src/generator/adapters.js +325 -0
- package/src/generator/api.d.ts +1 -0
- package/src/generator/api.js +1196 -0
- package/src/generator/check.js +355 -0
- package/src/generator/component-conformance.js +767 -0
- package/src/generator/components.js +39 -0
- package/src/generator/context/bundle.js +291 -0
- package/src/generator/context/diff.js +256 -0
- package/src/generator/context/digest.js +182 -0
- package/src/generator/context/domain-coverage.js +94 -0
- package/src/generator/context/domain-page.js +137 -0
- package/src/generator/context/index.js +42 -0
- package/src/generator/context/report.js +121 -0
- package/src/generator/context/shared.js +1397 -0
- package/src/generator/context/slice.js +703 -0
- package/src/generator/context/task-mode.js +466 -0
- package/src/generator/docs.js +327 -0
- package/src/generator/index.js +161 -0
- package/src/generator/native/parity-bundle.js +311 -0
- package/src/generator/output.js +300 -0
- package/src/generator/registry.js +482 -0
- package/src/generator/runtime/app-bundle.js +456 -0
- package/src/generator/runtime/bundle-shared.js +166 -0
- package/src/generator/runtime/compile-check.js +163 -0
- package/src/generator/runtime/deployment.js +287 -0
- package/src/generator/runtime/environment.js +635 -0
- package/src/generator/runtime/index.js +32 -0
- package/src/generator/runtime/runtime-check.js +554 -0
- package/src/generator/runtime/shared.js +515 -0
- package/src/generator/runtime/smoke.js +219 -0
- package/src/generator/schema.js +204 -0
- package/src/generator/sdlc/board.js +66 -0
- package/src/generator/sdlc/doc-page.js +53 -0
- package/src/generator/sdlc/index.js +23 -0
- package/src/generator/sdlc/release-notes.js +62 -0
- package/src/generator/sdlc/traceability-matrix.js +65 -0
- package/src/generator/shared.js +29 -0
- package/src/generator/surfaces/contracts.js +146 -0
- package/src/generator/surfaces/databases/contract.js +40 -0
- package/src/generator/surfaces/databases/index.js +84 -0
- package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
- package/src/generator/surfaces/databases/migration-plan.js +281 -0
- package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
- package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
- package/src/generator/surfaces/databases/postgres/index.js +9 -0
- package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
- package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
- package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
- package/src/generator/surfaces/databases/shared.d.ts +1 -0
- package/src/generator/surfaces/databases/shared.js +350 -0
- package/src/generator/surfaces/databases/snapshot.js +96 -0
- package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
- package/src/generator/surfaces/databases/sqlite/index.js +8 -0
- package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
- package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
- package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
- package/src/generator/surfaces/index.js +25 -0
- package/src/generator/surfaces/native/swiftui-app.js +38 -0
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
- package/src/generator/surfaces/services/express.d.ts +1 -0
- package/src/generator/surfaces/services/express.js +766 -0
- package/src/generator/surfaces/services/hono.d.ts +1 -0
- package/src/generator/surfaces/services/hono.js +204 -0
- package/src/generator/surfaces/services/index.js +42 -0
- package/src/generator/surfaces/services/persistence-wiring.js +240 -0
- package/src/generator/surfaces/services/runtime-helpers.js +631 -0
- package/src/generator/surfaces/services/server-contract.js +80 -0
- package/src/generator/surfaces/services/stateless.d.ts +1 -0
- package/src/generator/surfaces/services/stateless.js +97 -0
- package/src/generator/surfaces/shared.js +64 -0
- package/src/generator/surfaces/web/api-client.js +1 -0
- package/src/generator/surfaces/web/forms.js +1 -0
- package/src/generator/surfaces/web/index.d.ts +2 -0
- package/src/generator/surfaces/web/index.js +53 -0
- package/src/generator/surfaces/web/react-components.js +248 -0
- package/src/generator/surfaces/web/react.js +538 -0
- package/src/generator/surfaces/web/routes.js +1 -0
- package/src/generator/surfaces/web/screens.js +1 -0
- package/src/generator/surfaces/web/shared.js +369 -0
- package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
- package/src/generator/surfaces/web/sveltekit-components.js +234 -0
- package/src/generator/surfaces/web/sveltekit.js +426 -0
- package/src/generator/surfaces/web/ui-web-contract.js +65 -0
- package/src/generator/surfaces/web/vanilla.js +239 -0
- package/src/generator/verification.js +84 -0
- package/src/generator.js +1 -0
- package/src/import/core/context.js +52 -0
- package/src/import/core/contracts.js +23 -0
- package/src/import/core/registry.js +81 -0
- package/src/import/core/runner.js +646 -0
- package/src/import/core/shared.js +910 -0
- package/src/import/enrichers/auth-session.js +18 -0
- package/src/import/enrichers/django-rest.js +226 -0
- package/src/import/enrichers/doc-linking.js +20 -0
- package/src/import/enrichers/rails-controllers.js +246 -0
- package/src/import/enrichers/rails-models.js +130 -0
- package/src/import/enrichers/workflow-target-state.js +10 -0
- package/src/import/extractors/api/aspnet-core.js +304 -0
- package/src/import/extractors/api/django-routes.js +318 -0
- package/src/import/extractors/api/express.js +154 -0
- package/src/import/extractors/api/fastify.js +371 -0
- package/src/import/extractors/api/flutter-dio.js +135 -0
- package/src/import/extractors/api/generic-route-fallback.js +90 -0
- package/src/import/extractors/api/graphql-code-first.js +565 -0
- package/src/import/extractors/api/graphql-sdl.js +309 -0
- package/src/import/extractors/api/jaxrs.js +303 -0
- package/src/import/extractors/api/micronaut.js +213 -0
- package/src/import/extractors/api/next-route.js +50 -0
- package/src/import/extractors/api/next-server-action.js +51 -0
- package/src/import/extractors/api/nextauth.js +52 -0
- package/src/import/extractors/api/openapi-code.js +242 -0
- package/src/import/extractors/api/openapi.js +232 -0
- package/src/import/extractors/api/rails-routes.js +230 -0
- package/src/import/extractors/api/react-native-repository.js +128 -0
- package/src/import/extractors/api/retrofit.js +103 -0
- package/src/import/extractors/api/spring-web.js +372 -0
- package/src/import/extractors/api/swift-webapi.js +116 -0
- package/src/import/extractors/api/trpc.js +212 -0
- package/src/import/extractors/db/django-models.js +232 -0
- package/src/import/extractors/db/dotnet-models.js +93 -0
- package/src/import/extractors/db/drizzle.js +242 -0
- package/src/import/extractors/db/ef-core.js +221 -0
- package/src/import/extractors/db/flutter-entities.js +120 -0
- package/src/import/extractors/db/jpa.js +120 -0
- package/src/import/extractors/db/liquibase.js +180 -0
- package/src/import/extractors/db/mybatis-xml.js +145 -0
- package/src/import/extractors/db/prisma.js +185 -0
- package/src/import/extractors/db/rails-schema.js +175 -0
- package/src/import/extractors/db/react-native-entities.js +95 -0
- package/src/import/extractors/db/room.js +193 -0
- package/src/import/extractors/db/snapshot.js +112 -0
- package/src/import/extractors/db/sql.js +180 -0
- package/src/import/extractors/db/swiftdata.js +137 -0
- package/src/import/extractors/ui/android-compose.js +230 -0
- package/src/import/extractors/ui/backend-only.js +70 -0
- package/src/import/extractors/ui/blazor.js +227 -0
- package/src/import/extractors/ui/flutter-screens.js +152 -0
- package/src/import/extractors/ui/maui-xaml.js +135 -0
- package/src/import/extractors/ui/next-app-router.js +83 -0
- package/src/import/extractors/ui/next-pages-router.js +141 -0
- package/src/import/extractors/ui/razor-pages.js +181 -0
- package/src/import/extractors/ui/react-native-screens.js +166 -0
- package/src/import/extractors/ui/react-router.js +139 -0
- package/src/import/extractors/ui/sveltekit.js +123 -0
- package/src/import/extractors/ui/swiftui.js +193 -0
- package/src/import/extractors/ui/uikit.js +175 -0
- package/src/import/extractors/verification/generic.js +290 -0
- package/src/import/extractors/workflows/generic.js +137 -0
- package/src/import/index.js +7 -0
- package/src/import/provenance.js +158 -0
- package/src/new-project.js +2107 -0
- package/src/parser.js +439 -0
- package/src/policy/review-boundaries.js +165 -0
- package/src/project-config.js +535 -0
- package/src/proofs/backend-parity.js +19 -0
- package/src/proofs/contract-audit.js +220 -0
- package/src/proofs/ios-parity.js +7 -0
- package/src/proofs/issues-parity.js +10 -0
- package/src/proofs/web-parity.js +50 -0
- package/src/realization/api/build-api-realization.js +5 -0
- package/src/realization/api/index.js +1 -0
- package/src/realization/backend/build-backend-runtime-realization.js +82 -0
- package/src/realization/backend/index.d.ts +1 -0
- package/src/realization/backend/index.js +4 -0
- package/src/realization/db/build-db-realization.js +17 -0
- package/src/realization/db/index.js +3 -0
- package/src/realization/db/migration-plan.js +5 -0
- package/src/realization/db/snapshot.js +5 -0
- package/src/realization/ui/build-ui-shared-realization.js +305 -0
- package/src/realization/ui/build-web-realization.js +189 -0
- package/src/realization/ui/index.js +2 -0
- package/src/reconcile/docs.js +280 -0
- package/src/reconcile/index.js +3 -0
- package/src/reconcile/journeys.js +441 -0
- package/src/resolver/docs.js +1 -0
- package/src/resolver/enrich/acceptance-criterion.js +14 -0
- package/src/resolver/enrich/bug.js +12 -0
- package/src/resolver/enrich/component.js +2 -0
- package/src/resolver/enrich/index.js +1 -0
- package/src/resolver/enrich/pitch.js +18 -0
- package/src/resolver/enrich/requirement.js +20 -0
- package/src/resolver/enrich/task.js +16 -0
- package/src/resolver/expressions.js +1 -0
- package/src/resolver/index.js +2422 -0
- package/src/resolver/normalize.js +1 -0
- package/src/resolver.js +1 -0
- package/src/sdlc/adopt.js +65 -0
- package/src/sdlc/check.js +86 -0
- package/src/sdlc/dod/acceptance-criterion.js +22 -0
- package/src/sdlc/dod/bug.js +26 -0
- package/src/sdlc/dod/document.js +23 -0
- package/src/sdlc/dod/index.js +25 -0
- package/src/sdlc/dod/pitch.js +23 -0
- package/src/sdlc/dod/requirement.js +34 -0
- package/src/sdlc/dod/task.js +39 -0
- package/src/sdlc/explain.js +116 -0
- package/src/sdlc/history.js +80 -0
- package/src/sdlc/paths.js +11 -0
- package/src/sdlc/release.js +106 -0
- package/src/sdlc/scaffold.js +89 -0
- package/src/sdlc/status-filter.js +54 -0
- package/src/sdlc/transition.js +112 -0
- package/src/sdlc/transitions/acceptance-criterion.js +28 -0
- package/src/sdlc/transitions/bug.js +31 -0
- package/src/sdlc/transitions/document.js +29 -0
- package/src/sdlc/transitions/index.js +56 -0
- package/src/sdlc/transitions/pitch.js +34 -0
- package/src/sdlc/transitions/requirement.js +31 -0
- package/src/sdlc/transitions/task.js +34 -0
- package/src/template-trust.js +597 -0
- package/src/validator/expressions.js +1 -0
- package/src/validator/index.js +3424 -0
- package/src/validator/kinds.js +346 -0
- package/src/validator/per-kind/acceptance-criterion.js +91 -0
- package/src/validator/per-kind/bug.js +77 -0
- package/src/validator/per-kind/component.js +274 -0
- package/src/validator/per-kind/domain.js +205 -0
- package/src/validator/per-kind/pitch.js +101 -0
- package/src/validator/per-kind/requirement.js +75 -0
- package/src/validator/per-kind/task.js +96 -0
- package/src/validator/registry.js +1 -0
- package/src/validator/utils.js +12 -0
- package/src/validator.js +1 -0
- package/src/workflows.js +7597 -0
- package/src/workspace-docs.js +265 -0
- package/template-helpers/react.js +5 -0
- package/template-helpers/sveltekit.js +5 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
function titleCase(value) {
|
|
2
|
+
return String(value || "")
|
|
3
|
+
.split(/[_\-\s]+/)
|
|
4
|
+
.filter(Boolean)
|
|
5
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
6
|
+
.join(" ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function singularTitle(value) {
|
|
10
|
+
return titleCase(String(value || "").replace(/s$/i, ""));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function bundleKeyForConcept(conceptId) {
|
|
14
|
+
return String(conceptId || "")
|
|
15
|
+
.trim()
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "") || "candidate";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function primaryEntityIdForBundle(bundle) {
|
|
22
|
+
return (
|
|
23
|
+
bundle.mergeHints?.canonicalEntityTarget ||
|
|
24
|
+
bundle.entities?.[0]?.id_hint ||
|
|
25
|
+
(String(bundle.id || "").startsWith("entity_") ? bundle.id : null)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function canonicalJourneyCoverage(graph) {
|
|
30
|
+
const journeyDocs = (graph?.docs || []).filter((doc) => doc.kind === "journey");
|
|
31
|
+
return {
|
|
32
|
+
byEntityId: new Set(journeyDocs.flatMap((doc) => doc.relatedEntities || [])),
|
|
33
|
+
byCapabilityId: new Set(journeyDocs.flatMap((doc) => doc.relatedCapabilities || []))
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectJourneyGenerationContext(graph) {
|
|
38
|
+
const entities = graph.byKind.entity || [];
|
|
39
|
+
const capabilities = graph.byKind.capability || [];
|
|
40
|
+
const rules = graph.byKind.rule || [];
|
|
41
|
+
const projections = graph.byKind.projection || [];
|
|
42
|
+
const uiSharedScreens = projections
|
|
43
|
+
.filter((projection) => projection.platform === "ui_shared")
|
|
44
|
+
.flatMap((projection) => (projection.uiScreens || []).map((screen) => ({ ...screen, projectionId: projection.id })));
|
|
45
|
+
const canonicalJourneys = (graph.docs || []).filter((doc) => doc.kind === "journey");
|
|
46
|
+
const coveredEntityIds = new Set(canonicalJourneys.flatMap((doc) => doc.relatedEntities || []));
|
|
47
|
+
|
|
48
|
+
return entities
|
|
49
|
+
.map((entity) => {
|
|
50
|
+
const entitySlug = entity.id.replace(/^entity_/, "");
|
|
51
|
+
const title = singularTitle(entity.name || entitySlug);
|
|
52
|
+
const touchedCapabilities = capabilities.filter((capability) => {
|
|
53
|
+
const touchedIds = [
|
|
54
|
+
...(capability.reads || []).map((item) => item.id),
|
|
55
|
+
...(capability.creates || []).map((item) => item.id),
|
|
56
|
+
...(capability.updates || []).map((item) => item.id),
|
|
57
|
+
...(capability.deletes || []).map((item) => item.id)
|
|
58
|
+
];
|
|
59
|
+
return touchedIds.includes(entity.id) || capability.id.includes(entitySlug);
|
|
60
|
+
});
|
|
61
|
+
const screens = uiSharedScreens.filter((screen) => screen.id === entitySlug || screen.id.startsWith(`${entitySlug}_`));
|
|
62
|
+
const relatedRules = rules.filter((rule) => (rule.appliesTo || []).some((item) => item.id === entity.id));
|
|
63
|
+
const supportEntityIds = [
|
|
64
|
+
...new Set(
|
|
65
|
+
touchedCapabilities
|
|
66
|
+
.flatMap((capability) => [...(capability.reads || []), ...(capability.creates || []), ...(capability.updates || []), ...(capability.deletes || [])])
|
|
67
|
+
.map((item) => item.id)
|
|
68
|
+
.filter((id) => id && id !== entity.id)
|
|
69
|
+
)
|
|
70
|
+
].slice(0, 3);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
entity,
|
|
74
|
+
entitySlug,
|
|
75
|
+
title,
|
|
76
|
+
touchedCapabilities,
|
|
77
|
+
screens,
|
|
78
|
+
relatedRules,
|
|
79
|
+
supportEntityIds,
|
|
80
|
+
coveredByCanonicalJourney: coveredEntityIds.has(entity.id)
|
|
81
|
+
};
|
|
82
|
+
})
|
|
83
|
+
.filter((entry) => entry.touchedCapabilities.length > 0 || entry.screens.length > 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function journeyFlowGroups(entry) {
|
|
87
|
+
const creationCapabilities = entry.touchedCapabilities.filter((capability) => {
|
|
88
|
+
const id = capability.id;
|
|
89
|
+
return id.startsWith(`cap_create_${entry.entitySlug}`) || id.startsWith(`cap_list_${entry.entitySlug}`) || id.startsWith(`cap_get_${entry.entitySlug}`);
|
|
90
|
+
});
|
|
91
|
+
const lifecycleCapabilities = entry.touchedCapabilities.filter((capability) => {
|
|
92
|
+
const id = capability.id;
|
|
93
|
+
return (
|
|
94
|
+
id.startsWith(`cap_update_${entry.entitySlug}`) ||
|
|
95
|
+
id.startsWith(`cap_close_${entry.entitySlug}`) ||
|
|
96
|
+
id.startsWith(`cap_complete_${entry.entitySlug}`) ||
|
|
97
|
+
id.startsWith(`cap_delete_${entry.entitySlug}`) ||
|
|
98
|
+
id.startsWith(`cap_archive_${entry.entitySlug}`) ||
|
|
99
|
+
id.startsWith(`cap_submit_${entry.entitySlug}`) ||
|
|
100
|
+
id.startsWith(`cap_request_${entry.entitySlug}`)
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const groups = [];
|
|
105
|
+
if (creationCapabilities.length > 0) {
|
|
106
|
+
groups.push({
|
|
107
|
+
id: `${entry.entitySlug}_creation_and_discovery`,
|
|
108
|
+
title: `${entry.title} Creation and Discovery`,
|
|
109
|
+
type: "creation",
|
|
110
|
+
capabilities: creationCapabilities
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (lifecycleCapabilities.length > 0) {
|
|
114
|
+
groups.push({
|
|
115
|
+
id: `${entry.entitySlug}_update_and_lifecycle`,
|
|
116
|
+
title: `${entry.title} Update and Lifecycle`,
|
|
117
|
+
type: "lifecycle",
|
|
118
|
+
capabilities: lifecycleCapabilities
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (groups.length === 0 && entry.touchedCapabilities.length >= 2) {
|
|
122
|
+
groups.push({
|
|
123
|
+
id: `${entry.entitySlug}_core_flow`,
|
|
124
|
+
title: `${entry.title} Core Flow`,
|
|
125
|
+
type: "general",
|
|
126
|
+
capabilities: entry.touchedCapabilities
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return groups;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatCapabilityList(capabilities = []) {
|
|
133
|
+
if (capabilities.length === 0) {
|
|
134
|
+
return "_none_";
|
|
135
|
+
}
|
|
136
|
+
return capabilities.map((capability) => `\`${capability.id}\``).join(", ");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderJourneyDraftBody(entry, flow) {
|
|
140
|
+
const listScreen = entry.screens.find((screen) => ["list", "board", "calendar", "feed"].includes(screen.kind));
|
|
141
|
+
const detailScreen = entry.screens.find((screen) => screen.kind === "detail");
|
|
142
|
+
const createScreen = entry.screens.find((screen) => ["form", "wizard"].includes(screen.kind) && screen.submit?.id?.startsWith(`cap_create_${entry.entitySlug}`));
|
|
143
|
+
const editScreen = entry.screens.find((screen) => ["form", "wizard"].includes(screen.kind) && screen.submit?.id && !screen.submit.id.startsWith(`cap_create_${entry.entitySlug}`));
|
|
144
|
+
const primaryActorIds = [...new Set(flow.capabilities.flatMap((capability) => (capability.actors || []).map((actor) => actor.id)))];
|
|
145
|
+
const ruleMentions = entry.relatedRules.slice(0, 2).map((rule) => `\`${rule.id}\``);
|
|
146
|
+
const startSurface =
|
|
147
|
+
flow.type === "creation"
|
|
148
|
+
? createScreen?.title || listScreen?.title || `${entry.title} create flow`
|
|
149
|
+
: detailScreen?.title || editScreen?.title || `${entry.title} detail flow`;
|
|
150
|
+
const listSurface = listScreen?.title || `${entry.title} list`;
|
|
151
|
+
const detailSurface = detailScreen?.title || `${entry.title} detail`;
|
|
152
|
+
const lifecycleAction = flow.capabilities.find((capability) => /close|complete|archive|delete|submit|request/.test(capability.id)) || flow.capabilities.find((capability) => capability.id.startsWith(`cap_update_${entry.entitySlug}`));
|
|
153
|
+
|
|
154
|
+
const lines = [
|
|
155
|
+
"This draft journey was generated from canonical Topogram capabilities, UI screens, and related rules.",
|
|
156
|
+
"",
|
|
157
|
+
"Review and rewrite it before promotion as a canonical journey.",
|
|
158
|
+
"",
|
|
159
|
+
`The user intent centers on ${flow.type === "creation" ? `creating or locating ${entry.title.toLowerCase()} work safely` : flow.type === "lifecycle" ? `updating ${entry.title.toLowerCase()} work without losing lifecycle clarity` : `moving through the core ${entry.title.toLowerCase()} flow with confidence`}.`,
|
|
160
|
+
"",
|
|
161
|
+
"## Happy Path",
|
|
162
|
+
""
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
if (flow.type === "creation") {
|
|
166
|
+
lines.push(`1. The user starts from ${startSurface} and begins the ${entry.title.toLowerCase()} flow.`);
|
|
167
|
+
lines.push(`2. The system accepts the request through ${formatCapabilityList(flow.capabilities.filter((capability) => /create|list|get/.test(capability.id)))}.`);
|
|
168
|
+
lines.push(`3. The user can find the resulting ${entry.title.toLowerCase()} again in ${listSurface} and ${detailSurface}.`);
|
|
169
|
+
} else if (flow.type === "lifecycle") {
|
|
170
|
+
lines.push(`1. The user opens ${startSurface} and confirms the current ${entry.title.toLowerCase()} context.`);
|
|
171
|
+
lines.push(`2. The user progresses the flow through ${formatCapabilityList(flow.capabilities)}.`);
|
|
172
|
+
if (lifecycleAction) {
|
|
173
|
+
lines.push(`3. The flow can conclude with \`${lifecycleAction.id}\` once the ${entry.title.toLowerCase()} is ready.`);
|
|
174
|
+
} else {
|
|
175
|
+
lines.push(`3. The user returns to ${detailSurface} with the updated lifecycle state visible.`);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
lines.push(`1. The user starts from ${startSurface}.`);
|
|
179
|
+
lines.push(`2. The flow moves through ${formatCapabilityList(flow.capabilities)}.`);
|
|
180
|
+
lines.push(`3. The resulting ${entry.title.toLowerCase()} remains visible in ${listSurface} and ${detailSurface}.`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
lines.push("", "## Alternate Paths", "");
|
|
184
|
+
if (entry.relatedRules.length > 0) {
|
|
185
|
+
for (const rule of entry.relatedRules.slice(0, 2)) {
|
|
186
|
+
lines.push(`- The flow should stay aligned with ${rule.name ? `"${rule.name}"` : `\`${rule.id}\``} instead of silently allowing invalid state transitions.`);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
lines.push(`- If the request is invalid or unauthorized, the flow should fail clearly instead of leaving the ${entry.title.toLowerCase()} in an ambiguous state.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lines.push("", "## Change Review Notes", "");
|
|
193
|
+
lines.push(`Review this journey when changing ${entry.title.toLowerCase()} screens, ${entry.title.toLowerCase()} capability contracts, or related rules${ruleMentions.length ? ` such as ${ruleMentions.join(", ")}` : ""}.`);
|
|
194
|
+
if (primaryActorIds.length > 0) {
|
|
195
|
+
lines.push("", `Primary actors: ${primaryActorIds.map((id) => `\`${id}\``).join(", ")}`);
|
|
196
|
+
}
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function buildJourneyDrafts(graph) {
|
|
201
|
+
const draftEntries = [];
|
|
202
|
+
const skippedEntities = [];
|
|
203
|
+
for (const entry of collectJourneyGenerationContext(graph)) {
|
|
204
|
+
if (entry.coveredByCanonicalJourney) {
|
|
205
|
+
skippedEntities.push({
|
|
206
|
+
entity_id: entry.entity.id,
|
|
207
|
+
reason: "canonical_journey_exists"
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const flow of journeyFlowGroups(entry)) {
|
|
213
|
+
const relatedCapabilities = [...new Set(flow.capabilities.map((capability) => capability.id))].sort();
|
|
214
|
+
const relatedActors = [...new Set(flow.capabilities.flatMap((capability) => (capability.actors || []).map((actor) => actor.id)))].sort();
|
|
215
|
+
const relatedRoles = [...new Set(flow.capabilities.flatMap((capability) => (capability.roles || []).map((role) => role.id)))].sort();
|
|
216
|
+
const relatedRules = [...new Set(entry.relatedRules.map((rule) => rule.id))].sort();
|
|
217
|
+
const relatedProjections = [...new Set(entry.screens.map((screen) => screen.projectionId).filter(Boolean))].sort();
|
|
218
|
+
const provenance = [
|
|
219
|
+
...relatedCapabilities.map((id) => `capability:${id}`),
|
|
220
|
+
...entry.screens.map((screen) => `screen:${screen.id}`)
|
|
221
|
+
].slice(0, 8);
|
|
222
|
+
const metadata = {
|
|
223
|
+
id: flow.id,
|
|
224
|
+
kind: "journey",
|
|
225
|
+
title: flow.title,
|
|
226
|
+
status: "inferred",
|
|
227
|
+
summary: `Draft ${entry.title.toLowerCase()} journey inferred from capabilities and UI surfaces.`,
|
|
228
|
+
source_of_truth: "generated",
|
|
229
|
+
confidence: "medium",
|
|
230
|
+
review_required: true,
|
|
231
|
+
related_entities: [entry.entity.id, ...entry.supportEntityIds].slice(0, 4),
|
|
232
|
+
related_capabilities: relatedCapabilities,
|
|
233
|
+
related_actors: relatedActors,
|
|
234
|
+
related_roles: relatedRoles,
|
|
235
|
+
related_rules: relatedRules,
|
|
236
|
+
related_projections: relatedProjections,
|
|
237
|
+
provenance,
|
|
238
|
+
tags: ["generated", "journey", "draft"]
|
|
239
|
+
};
|
|
240
|
+
const relativePath = `candidates/docs/journeys/${flow.id.replaceAll("_", "-")}.md`;
|
|
241
|
+
draftEntries.push({
|
|
242
|
+
id: flow.id,
|
|
243
|
+
title: flow.title,
|
|
244
|
+
type: flow.type,
|
|
245
|
+
entity_id: entry.entity.id,
|
|
246
|
+
path: relativePath,
|
|
247
|
+
related_capabilities: relatedCapabilities,
|
|
248
|
+
related_entities: metadata.related_entities,
|
|
249
|
+
metadata,
|
|
250
|
+
body: renderJourneyDraftBody(entry, flow)
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
drafts: draftEntries.sort((left, right) => left.path.localeCompare(right.path)),
|
|
257
|
+
skippedEntities: skippedEntities.sort((left, right) => left.entity_id.localeCompare(right.entity_id))
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function buildBundleJourneyDraft(bundle) {
|
|
262
|
+
const relatedCapabilities = [...new Set((bundle.capabilities || []).map((entry) => entry.id_hint))].sort();
|
|
263
|
+
const relatedWorkflows = [...new Set((bundle.workflows || []).map((entry) => entry.id_hint))].sort();
|
|
264
|
+
const relatedActors = [...new Set((bundle.actors || []).map((entry) => entry.id_hint))].sort();
|
|
265
|
+
const relatedRoles = [...new Set((bundle.roles || []).map((entry) => entry.id_hint))].sort();
|
|
266
|
+
const primaryEntityId = primaryEntityIdForBundle(bundle);
|
|
267
|
+
const relatedEntities = [...new Set([primaryEntityId, ...(bundle.entities || []).map((entry) => entry.id_hint)].filter(Boolean))].slice(0, 4);
|
|
268
|
+
const routePaths = [...new Set((bundle.uiRoutes || []).map((entry) => entry.path).filter(Boolean))];
|
|
269
|
+
const screenIds = [...new Set((bundle.screens || []).map((entry) => entry.id_hint))];
|
|
270
|
+
const screenKinds = [...new Set((bundle.screens || []).map((entry) => entry.screen_kind).filter(Boolean))];
|
|
271
|
+
const createCapabilities = relatedCapabilities.filter((id) => /^cap_create_/.test(id));
|
|
272
|
+
const browseCapabilities = relatedCapabilities.filter((id) => /^cap_(list|get)_/.test(id));
|
|
273
|
+
const lifecycleCapabilities = relatedCapabilities.filter((id) => /^cap_(update|close|complete|archive|delete|submit|request)_/.test(id));
|
|
274
|
+
const interactionCapabilities = relatedCapabilities.filter((id) => /^cap_(favorite|unfavorite|follow|unfollow|vote|like|unlike)_/.test(id));
|
|
275
|
+
const authCapabilities = relatedCapabilities.filter((id) => /^cap_(sign_in|sign_out|register|authenticate|login|logout)_/.test(id));
|
|
276
|
+
const participantActors = relatedActors;
|
|
277
|
+
const participantRoles = relatedRoles;
|
|
278
|
+
const hasListDetail = browseCapabilities.some((id) => /^cap_list_/.test(id)) && browseCapabilities.some((id) => /^cap_get_/.test(id));
|
|
279
|
+
const hasCreateAndLifecycle = createCapabilities.length > 0 && lifecycleCapabilities.length > 0;
|
|
280
|
+
const hasWorkflowEvidence = relatedWorkflows.length > 0;
|
|
281
|
+
const flowShape =
|
|
282
|
+
authCapabilities.length > 0 ? "auth" :
|
|
283
|
+
hasCreateAndLifecycle && hasListDetail ? "create_browse_lifecycle" :
|
|
284
|
+
hasListDetail && lifecycleCapabilities.length > 0 ? "browse_lifecycle" :
|
|
285
|
+
hasListDetail ? "browse_detail" :
|
|
286
|
+
createCapabilities.length > 0 ? "create" :
|
|
287
|
+
lifecycleCapabilities.length > 0 ? "lifecycle" :
|
|
288
|
+
interactionCapabilities.length > 0 ? "interaction" :
|
|
289
|
+
hasWorkflowEvidence ? "workflow" :
|
|
290
|
+
"general";
|
|
291
|
+
|
|
292
|
+
const humanLabel = bundle.label || titleCase(String(primaryEntityId || bundle.id || "journey").replace(/^entity_/, ""));
|
|
293
|
+
const title =
|
|
294
|
+
flowShape === "auth" ? `${humanLabel} Sign-In and Session Flow` :
|
|
295
|
+
flowShape === "create_browse_lifecycle" ? `${humanLabel} Creation, Detail, and Lifecycle Flow` :
|
|
296
|
+
flowShape === "browse_lifecycle" ? `${humanLabel} Detail and Lifecycle Flow` :
|
|
297
|
+
flowShape === "browse_detail" ? `${humanLabel} Browse and Detail Flow` :
|
|
298
|
+
flowShape === "create" ? `${humanLabel} Creation Flow` :
|
|
299
|
+
flowShape === "lifecycle" ? `${humanLabel} Lifecycle Flow` :
|
|
300
|
+
flowShape === "interaction" ? `${humanLabel} Interaction Flow` :
|
|
301
|
+
`${humanLabel} Core Flow`;
|
|
302
|
+
|
|
303
|
+
const idStem = String(primaryEntityId || bundle.id || "journey").replace(/^entity_/, "");
|
|
304
|
+
const id = `${idStem}_journey`;
|
|
305
|
+
const sourcePath = `candidates/reconcile/model/bundles/${bundle.slug}/docs/journeys/${id}.md`;
|
|
306
|
+
const canonicalRelPath = `docs/journeys/${id}.md`;
|
|
307
|
+
const participantText = [...participantActors, ...participantRoles].length > 0
|
|
308
|
+
? `The strongest inferred participants are ${[...participantActors, ...participantRoles].map((entry) => `\`${entry}\``).join(", ")}.`
|
|
309
|
+
: "The strongest inferred participant is still unclear and should be confirmed during review.";
|
|
310
|
+
const routeEvidence = routePaths.length > 0;
|
|
311
|
+
const screenEvidence = screenIds.length > 0;
|
|
312
|
+
const startSurface = routeEvidence
|
|
313
|
+
? routePaths.slice(0, 2).map((item) => `\`${item}\``).join(" or ")
|
|
314
|
+
: screenEvidence
|
|
315
|
+
? screenIds.slice(0, 2).map((item) => `\`${item}\``).join(" or ")
|
|
316
|
+
: `the ${humanLabel.toLowerCase()} API surface`;
|
|
317
|
+
const continuationSurface = screenEvidence
|
|
318
|
+
? `${screenKinds.length > 0 ? screenKinds.join(", ") : "screen"} surfaces ${screenIds.slice(0, 3).map((item) => `\`${item}\``).join(", ")}`
|
|
319
|
+
: routeEvidence
|
|
320
|
+
? `the recovered route structure around ${routePaths.slice(0, 3).map((item) => `\`${item}\``).join(", ")}`
|
|
321
|
+
: `the recovered ${humanLabel.toLowerCase()} lifecycle`;
|
|
322
|
+
const routeText = routePaths.length > 0
|
|
323
|
+
? `Recovered route evidence includes ${routePaths.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}.`
|
|
324
|
+
: "No strong route evidence was recovered, so this draft leans on capability and workflow evidence.";
|
|
325
|
+
const screenText = screenIds.length > 0
|
|
326
|
+
? `Recovered UI surface hints include ${screenIds.slice(0, 4).map((entry) => `\`${entry}\``).join(", ")}${screenKinds.length > 0 ? ` (${screenKinds.join(", ")})` : ""}.`
|
|
327
|
+
: "No strong screen evidence was recovered, so UI touchpoints still need review.";
|
|
328
|
+
|
|
329
|
+
let happyPathLines;
|
|
330
|
+
if (flowShape === "auth") {
|
|
331
|
+
happyPathLines = [
|
|
332
|
+
`1. ${participantActors.length || participantRoles.length ? `${[...participantActors, ...participantRoles].map((entry) => `\`${entry}\``).join(", ")} enter` : "A user enters"} the flow through the recovered account/auth surface.`,
|
|
333
|
+
`2. The system moves through ${authCapabilities.map((entry) => `\`${entry}\``).join(", ")} to establish authenticated state.`,
|
|
334
|
+
`3. The recovered session/account state is then available to the rest of the application surface.`
|
|
335
|
+
];
|
|
336
|
+
} else if (flowShape === "create_browse_lifecycle") {
|
|
337
|
+
happyPathLines = [
|
|
338
|
+
`1. The flow starts by creating or submitting ${humanLabel.toLowerCase()} data through ${createCapabilities.map((entry) => `\`${entry}\``).join(", ")}.`,
|
|
339
|
+
`2. The created record becomes visible again through ${browseCapabilities.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}.`,
|
|
340
|
+
`3. Follow-up lifecycle progress can continue through ${lifecycleCapabilities.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}.`
|
|
341
|
+
];
|
|
342
|
+
} else if (flowShape === "browse_lifecycle") {
|
|
343
|
+
happyPathLines = [
|
|
344
|
+
`1. The flow starts by locating the current ${humanLabel.toLowerCase()} state through ${browseCapabilities.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}.`,
|
|
345
|
+
`2. The user or system then advances the lifecycle through ${lifecycleCapabilities.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}.`,
|
|
346
|
+
`3. The updated state remains visible through the same detail-oriented surface.`
|
|
347
|
+
];
|
|
348
|
+
} else if (flowShape === "browse_detail") {
|
|
349
|
+
happyPathLines = [
|
|
350
|
+
`1. The flow starts by listing or locating ${humanLabel.toLowerCase()} data through ${browseCapabilities.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}.`,
|
|
351
|
+
`2. The user drills into the recovered detail surface for one record.`,
|
|
352
|
+
`3. The system preserves enough context to move back to the broader list or summary view.`
|
|
353
|
+
];
|
|
354
|
+
} else if (flowShape === "lifecycle") {
|
|
355
|
+
happyPathLines = [
|
|
356
|
+
`1. The flow starts from an existing ${humanLabel.toLowerCase()} record.`,
|
|
357
|
+
`2. The lifecycle proceeds through ${lifecycleCapabilities.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}.`,
|
|
358
|
+
`3. The updated lifecycle state becomes visible in the recovered output surface.`
|
|
359
|
+
];
|
|
360
|
+
} else if (flowShape === "interaction") {
|
|
361
|
+
happyPathLines = [
|
|
362
|
+
`1. The flow starts from an existing ${humanLabel.toLowerCase()} record or feed surface.`,
|
|
363
|
+
`2. The user interaction proceeds through ${interactionCapabilities.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}.`,
|
|
364
|
+
`3. The resulting state is reflected back in the surrounding list, detail, or profile context.`
|
|
365
|
+
];
|
|
366
|
+
} else {
|
|
367
|
+
happyPathLines = [
|
|
368
|
+
`1. ${participantActors.length || participantRoles.length ? `${[...participantActors, ...participantRoles].map((entry) => `\`${entry}\``).join(", ")} enter` : "A user enters"} the flow through ${startSurface}.`,
|
|
369
|
+
`2. The system accepts the request through ${relatedCapabilities.length > 0 ? relatedCapabilities.slice(0, 4).map((entry) => `\`${entry}\``).join(", ") : "_no inferred capability_"}.`,
|
|
370
|
+
`3. The resulting state remains visible through ${continuationSurface}.`
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const alternatePathLines = [];
|
|
375
|
+
if (relatedWorkflows.length > 0) {
|
|
376
|
+
alternatePathLines.push(`- Workflow evidence suggests ${relatedWorkflows.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")} may impose additional state or review constraints.`);
|
|
377
|
+
}
|
|
378
|
+
if (routePaths.length === 0 && screenIds.length === 0) {
|
|
379
|
+
alternatePathLines.push("- UI routing evidence is sparse, so this draft should be reviewed against the live app before promotion.");
|
|
380
|
+
}
|
|
381
|
+
if (alternatePathLines.length === 0) {
|
|
382
|
+
alternatePathLines.push("- Missing, unauthorized, or invalid requests should fail clearly instead of leaving the flow in an ambiguous state.");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const body = [
|
|
386
|
+
"Candidate journey inferred during reconcile from bundle-local capability, UI, workflow, and participant evidence.",
|
|
387
|
+
"",
|
|
388
|
+
participantText,
|
|
389
|
+
routeText,
|
|
390
|
+
screenText,
|
|
391
|
+
"",
|
|
392
|
+
"Review and rewrite this draft before promoting it as a canonical journey.",
|
|
393
|
+
"",
|
|
394
|
+
"## Happy Path",
|
|
395
|
+
"",
|
|
396
|
+
...happyPathLines,
|
|
397
|
+
"",
|
|
398
|
+
"## Alternate Paths",
|
|
399
|
+
"",
|
|
400
|
+
...alternatePathLines,
|
|
401
|
+
"",
|
|
402
|
+
"## Change Review Notes",
|
|
403
|
+
"",
|
|
404
|
+
`Review this journey when changing ${relatedCapabilities.length > 0 ? relatedCapabilities.slice(0, 4).map((entry) => `\`${entry}\``).join(", ") : "the recovered capability surface"}${relatedWorkflows.length > 0 ? `, related workflows such as ${relatedWorkflows.slice(0, 3).map((entry) => `\`${entry}\``).join(", ")}` : ""}, or the recovered ${screenIds.length > 0 ? "UI screens" : "API routes"}.`
|
|
405
|
+
].join("\n");
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
id,
|
|
409
|
+
kind: "journey",
|
|
410
|
+
title,
|
|
411
|
+
status: "inferred",
|
|
412
|
+
summary: `Draft ${humanLabel.toLowerCase()} journey inferred during brownfield reconcile.`,
|
|
413
|
+
source_of_truth: "imported",
|
|
414
|
+
confidence: bundle.confidence || "medium",
|
|
415
|
+
review_required: true,
|
|
416
|
+
related_entities: relatedEntities,
|
|
417
|
+
related_capabilities: relatedCapabilities,
|
|
418
|
+
related_actors: relatedActors,
|
|
419
|
+
related_roles: relatedRoles,
|
|
420
|
+
related_workflows: relatedWorkflows,
|
|
421
|
+
tags: ["import", "journey", "draft"],
|
|
422
|
+
source_path: sourcePath,
|
|
423
|
+
canonical_rel_path: canonicalRelPath,
|
|
424
|
+
body
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function shouldAddBundleJourneyDraft(bundle, graph) {
|
|
429
|
+
const primaryEntityId = primaryEntityIdForBundle(bundle);
|
|
430
|
+
const coverage = canonicalJourneyCoverage(graph);
|
|
431
|
+
const capabilityIds = new Set((bundle.capabilities || []).map((entry) => entry.id_hint));
|
|
432
|
+
return (
|
|
433
|
+
((bundle.capabilities || []).length > 0 || (bundle.screens || []).length > 0 || (bundle.workflows || []).length > 0) &&
|
|
434
|
+
!coverage.byEntityId.has(primaryEntityId) &&
|
|
435
|
+
![...capabilityIds].some((id) => coverage.byCapabilityId.has(id))
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function bundleKeyForJourneyConcept(conceptId) {
|
|
440
|
+
return bundleKeyForConcept(String(conceptId || "").replace(/^entity_/, "").replace(/^enum_/, ""));
|
|
441
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Documentation normalization remains orchestrated from index.js after the initial split.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Resolver enrichment for acceptance criteria.
|
|
2
|
+
//
|
|
3
|
+
// Back-link arrays:
|
|
4
|
+
// tasks — tasks whose `acceptance_refs` includes this AC
|
|
5
|
+
// verifications — verifications whose `acceptance_refs` includes this AC
|
|
6
|
+
// supersededBy — ACs that supersede *this* one
|
|
7
|
+
|
|
8
|
+
export function enrichAcceptanceCriterion(ac, index) {
|
|
9
|
+
return {
|
|
10
|
+
tasks: (index.tasksByAcceptanceRef.get(ac.id) || []).slice().sort(),
|
|
11
|
+
verifications: (index.verificationsByAcceptanceRef.get(ac.id) || []).slice().sort(),
|
|
12
|
+
supersededBy: (index.supersededByAcs.get(ac.id) || []).slice().sort()
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Resolver enrichment for bugs.
|
|
2
|
+
//
|
|
3
|
+
// Back-link arrays:
|
|
4
|
+
// verifiedBy — verifications whose `fixes_bugs` includes this bug
|
|
5
|
+
// (typically same set as bug.fixedInVerification, but the
|
|
6
|
+
// author may set only one side; we surface both)
|
|
7
|
+
|
|
8
|
+
export function enrichBug(bug, index) {
|
|
9
|
+
return {
|
|
10
|
+
verifiedBy: (index.verificationsFixingBug.get(bug.id) || []).slice().sort()
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Statement enrichment remains orchestrated from ../index.js after the initial split.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Resolver enrichment for pitches.
|
|
2
|
+
//
|
|
3
|
+
// Builds back-link arrays so consumers can ask a pitch "who follows you?"
|
|
4
|
+
// without re-walking the workspace each time. All lists are arrays of
|
|
5
|
+
// statement ids (strings); look-ups happen against `byId` in the caller.
|
|
6
|
+
//
|
|
7
|
+
// Inputs:
|
|
8
|
+
// pitch — the normalized pitch statement
|
|
9
|
+
// index — { requirementsByPitch, decisionsByPitch }
|
|
10
|
+
//
|
|
11
|
+
// Output is merged into the statement by `resolveWorkspace`.
|
|
12
|
+
|
|
13
|
+
export function enrichPitch(pitch, index) {
|
|
14
|
+
return {
|
|
15
|
+
requirements: (index.requirementsByPitch.get(pitch.id) || []).slice().sort(),
|
|
16
|
+
decisionsFromPitch: (index.decisionsByPitch.get(pitch.id) || []).slice().sort()
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Resolver enrichment for requirements.
|
|
2
|
+
//
|
|
3
|
+
// Back-link arrays:
|
|
4
|
+
// acceptanceCriteria — ACs whose `requirement` points here
|
|
5
|
+
// tasks — tasks whose `satisfies` includes this requirement
|
|
6
|
+
// verifications — verifications whose `requirement_refs` includes this
|
|
7
|
+
// supersededBy — requirements that supersede *this* one
|
|
8
|
+
// documents — docs whose `satisfies` frontmatter points here
|
|
9
|
+
// rules — rules whose `from_requirement` is this requirement
|
|
10
|
+
|
|
11
|
+
export function enrichRequirement(requirement, index) {
|
|
12
|
+
return {
|
|
13
|
+
acceptanceCriteria: (index.acsByRequirement.get(requirement.id) || []).slice().sort(),
|
|
14
|
+
tasks: (index.tasksBySatisfiedRequirement.get(requirement.id) || []).slice().sort(),
|
|
15
|
+
verifications: (index.verificationsByRequirementRef.get(requirement.id) || []).slice().sort(),
|
|
16
|
+
supersededBy: (index.supersededByRequirements.get(requirement.id) || []).slice().sort(),
|
|
17
|
+
documents: (index.documentsBySatisfies.get(requirement.id) || []).slice().sort(),
|
|
18
|
+
rules: (index.rulesByFromRequirement.get(requirement.id) || []).slice().sort()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Resolver enrichment for tasks.
|
|
2
|
+
//
|
|
3
|
+
// Back-link arrays:
|
|
4
|
+
// blockingMe — tasks whose `blocks` references this task (reciprocal of blocked_by)
|
|
5
|
+
// blockedByMe — tasks whose `blocked_by` references this task (reciprocal of blocks)
|
|
6
|
+
//
|
|
7
|
+
// Note: we deliberately compute *both* directions even though `blocks` and
|
|
8
|
+
// `blocked_by` are reciprocal, because authors only write one side. The
|
|
9
|
+
// resolver bridges them.
|
|
10
|
+
|
|
11
|
+
export function enrichTask(task, index) {
|
|
12
|
+
return {
|
|
13
|
+
blockingMe: (index.tasksThatBlockTarget.get(task.id) || []).slice().sort(),
|
|
14
|
+
blockedByMe: (index.tasksBlockedByTarget.get(task.id) || []).slice().sort()
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Expression normalization remains orchestrated from index.js after the initial split.
|