shift-ax 0.3.0
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/LICENSE +21 -0
- package/README.ko.md +145 -0
- package/README.md +143 -0
- package/dist/adapters/claude-code/adapter.js +90 -0
- package/dist/adapters/codex/adapter.js +94 -0
- package/dist/adapters/contracts.js +7 -0
- package/dist/adapters/index.js +12 -0
- package/dist/core/context/context-bundle.js +300 -0
- package/dist/core/context/discovery.js +82 -0
- package/dist/core/context/global-index-authoring.js +199 -0
- package/dist/core/context/global-knowledge-updates.js +116 -0
- package/dist/core/context/glossary.js +73 -0
- package/dist/core/context/guided-onboarding.js +233 -0
- package/dist/core/context/index-authoring.js +47 -0
- package/dist/core/context/index-resolver.js +78 -0
- package/dist/core/context/onboarding.js +186 -0
- package/dist/core/diagnostics/doctor.js +154 -0
- package/dist/core/finalization/commit-message.js +76 -0
- package/dist/core/finalization/commit-workflow.js +131 -0
- package/dist/core/memory/consolidation.js +99 -0
- package/dist/core/memory/decision-register.js +141 -0
- package/dist/core/memory/entity-memory.js +25 -0
- package/dist/core/memory/learned-debug.js +52 -0
- package/dist/core/memory/summary-checkpoints.js +9 -0
- package/dist/core/memory/thread-promotion.js +22 -0
- package/dist/core/memory/threads.js +66 -0
- package/dist/core/memory/topic-recall.js +52 -0
- package/dist/core/observability/context-health.js +15 -0
- package/dist/core/observability/context-monitor.js +29 -0
- package/dist/core/observability/state-handoff.js +78 -0
- package/dist/core/observability/topic-status.js +40 -0
- package/dist/core/observability/topics-status.js +26 -0
- package/dist/core/observability/verification-debt.js +82 -0
- package/dist/core/planning/brainstorm.js +120 -0
- package/dist/core/planning/escalation.js +69 -0
- package/dist/core/planning/execution-handoff.js +61 -0
- package/dist/core/planning/execution-launch.js +156 -0
- package/dist/core/planning/execution-orchestrator.js +87 -0
- package/dist/core/planning/feedback-reactions.js +75 -0
- package/dist/core/planning/lifecycle-events.js +45 -0
- package/dist/core/planning/plan-review.js +76 -0
- package/dist/core/planning/policy-context-sync.js +154 -0
- package/dist/core/planning/request-pipeline.js +386 -0
- package/dist/core/planning/workflow-state.js +18 -0
- package/dist/core/policies/project-profile.js +28 -0
- package/dist/core/policies/team-preferences.js +17 -0
- package/dist/core/review/aggregate-reviews.js +129 -0
- package/dist/core/review/run-lanes.js +376 -0
- package/dist/core/settings/global-context-home.js +28 -0
- package/dist/core/settings/project-settings.js +37 -0
- package/dist/core/shell/platform-shell.js +144 -0
- package/dist/core/topics/bootstrap.js +119 -0
- package/dist/core/topics/topic-artifacts.js +36 -0
- package/dist/core/topics/worktree-runtime.js +141 -0
- package/dist/core/topics/worktree.js +8 -0
- package/dist/platform/claude-code/bootstrap.js +66 -0
- package/dist/platform/claude-code/execution.js +157 -0
- package/dist/platform/claude-code/scaffold/CLAUDE.template.md +40 -0
- package/dist/platform/claude-code/scaffold/commands/doctor.template.md +11 -0
- package/dist/platform/claude-code/scaffold/commands/export-context.template.md +20 -0
- package/dist/platform/claude-code/scaffold/commands/onboard.template.md +43 -0
- package/dist/platform/claude-code/scaffold/commands/onboarding.template.md +43 -0
- package/dist/platform/claude-code/scaffold/commands/request.template.md +19 -0
- package/dist/platform/claude-code/scaffold/commands/resume.template.md +12 -0
- package/dist/platform/claude-code/scaffold/commands/review.template.md +10 -0
- package/dist/platform/claude-code/scaffold/commands/status.template.md +14 -0
- package/dist/platform/claude-code/scaffold/commands/topics.template.md +10 -0
- package/dist/platform/claude-code/scaffold/hooks/shift-ax-session-start.template.md +29 -0
- package/dist/platform/claude-code/tmux.js +35 -0
- package/dist/platform/claude-code/upstream/tmux/imported/detached-session.js +40 -0
- package/dist/platform/claude-code/upstream/tmux/imported/session-name.js +19 -0
- package/dist/platform/claude-code/upstream/worktree/imported/get-worktree-root.js +39 -0
- package/dist/platform/claude-code/upstream/worktree/imported/managed-worktree.js +77 -0
- package/dist/platform/claude-code/worktree.js +79 -0
- package/dist/platform/codex/bootstrap.js +69 -0
- package/dist/platform/codex/execution.js +163 -0
- package/dist/platform/codex/scaffold/AGENTS.template.md +40 -0
- package/dist/platform/codex/scaffold/prompts/doctor.template.md +11 -0
- package/dist/platform/codex/scaffold/prompts/export-context.template.md +20 -0
- package/dist/platform/codex/scaffold/prompts/onboard.template.md +43 -0
- package/dist/platform/codex/scaffold/prompts/onboarding.template.md +43 -0
- package/dist/platform/codex/scaffold/prompts/request.template.md +19 -0
- package/dist/platform/codex/scaffold/prompts/resume.template.md +14 -0
- package/dist/platform/codex/scaffold/prompts/review.template.md +10 -0
- package/dist/platform/codex/scaffold/prompts/shift-ax-bootstrap.template.md +23 -0
- package/dist/platform/codex/scaffold/prompts/status.template.md +14 -0
- package/dist/platform/codex/scaffold/prompts/topics.template.md +10 -0
- package/dist/platform/codex/scaffold/skills/doctor/SKILL.template.md +11 -0
- package/dist/platform/codex/scaffold/skills/export-context/SKILL.template.md +20 -0
- package/dist/platform/codex/scaffold/skills/onboard/SKILL.template.md +43 -0
- package/dist/platform/codex/scaffold/skills/request/SKILL.template.md +19 -0
- package/dist/platform/codex/scaffold/skills/resume/SKILL.template.md +14 -0
- package/dist/platform/codex/scaffold/skills/review/SKILL.template.md +10 -0
- package/dist/platform/codex/scaffold/skills/status/SKILL.template.md +14 -0
- package/dist/platform/codex/scaffold/skills/topics/SKILL.template.md +10 -0
- package/dist/platform/codex/tmux.js +45 -0
- package/dist/platform/codex/upstream/tmux/imported/resize-hook-registration.js +37 -0
- package/dist/platform/codex/upstream/tmux/imported/resize-hooks.js +29 -0
- package/dist/platform/codex/upstream/tmux/imported/sanitize-team-name.js +18 -0
- package/dist/platform/codex/upstream/worktree/imported/managed-worktree.js +208 -0
- package/dist/platform/codex/upstream/worktree/imported/resolve-repo-root.js +14 -0
- package/dist/platform/codex/worktree.js +99 -0
- package/dist/platform/index.js +10 -0
- package/dist/platform/product-shell-commands.js +17 -0
- package/dist/platform/scaffold.js +16 -0
- package/dist/platform/upstream-imports.js +5 -0
- package/dist/scripts/ax-approve-plan.js +30 -0
- package/dist/scripts/ax-bootstrap-assets.js +19 -0
- package/dist/scripts/ax-bootstrap-topic.js +24 -0
- package/dist/scripts/ax-build-context-bundle.js +35 -0
- package/dist/scripts/ax-checkpoint-context.js +22 -0
- package/dist/scripts/ax-consolidate-memory.js +7 -0
- package/dist/scripts/ax-context-health.js +26 -0
- package/dist/scripts/ax-decisions.js +32 -0
- package/dist/scripts/ax-doctor.js +25 -0
- package/dist/scripts/ax-entity-memory.js +19 -0
- package/dist/scripts/ax-export-context.js +8 -0
- package/dist/scripts/ax-finalize-commit.js +23 -0
- package/dist/scripts/ax-init-context.js +41 -0
- package/dist/scripts/ax-launch-execution.js +24 -0
- package/dist/scripts/ax-learned-debug-save.js +30 -0
- package/dist/scripts/ax-learned-debug.js +12 -0
- package/dist/scripts/ax-monitor-context.js +28 -0
- package/dist/scripts/ax-onboard-context.js +112 -0
- package/dist/scripts/ax-pause-work.js +33 -0
- package/dist/scripts/ax-platform-manifest.js +19 -0
- package/dist/scripts/ax-promote-thread.js +20 -0
- package/dist/scripts/ax-react-feedback.js +28 -0
- package/dist/scripts/ax-recall-topics.js +20 -0
- package/dist/scripts/ax-recall.js +58 -0
- package/dist/scripts/ax-refresh-state.js +15 -0
- package/dist/scripts/ax-resolve-context.js +34 -0
- package/dist/scripts/ax-review.js +24 -0
- package/dist/scripts/ax-run-request.js +198 -0
- package/dist/scripts/ax-scaffold-build.js +19 -0
- package/dist/scripts/ax-shell.js +123 -0
- package/dist/scripts/ax-sync-policy-context.js +40 -0
- package/dist/scripts/ax-team-preferences.js +20 -0
- package/dist/scripts/ax-thread-save.js +26 -0
- package/dist/scripts/ax-threads.js +11 -0
- package/dist/scripts/ax-topic-status.js +18 -0
- package/dist/scripts/ax-topics-status.js +22 -0
- package/dist/scripts/ax-verification-debt.js +22 -0
- package/dist/scripts/ax-worktree-create.js +22 -0
- package/dist/scripts/ax-worktree-plan.js +18 -0
- package/dist/scripts/ax-worktree-remove.js +18 -0
- package/dist/scripts/ax.js +132 -0
- package/package.json +71 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { searchDecisionMemory } from '../memory/decision-register.js';
|
|
4
|
+
import { searchPastTopics } from '../memory/topic-recall.js';
|
|
5
|
+
import { getRootDirFromTopicDir } from '../topics/topic-artifacts.js';
|
|
6
|
+
import { parseIndexDocument, resolveContextFromIndex, } from './index-resolver.js';
|
|
7
|
+
import { getGlobalContextHome } from '../settings/global-context-home.js';
|
|
8
|
+
export function classifyContextBundle(bundle) {
|
|
9
|
+
if (bundle.issues.length > 0) {
|
|
10
|
+
return {
|
|
11
|
+
status: 'critical',
|
|
12
|
+
recommendation: 'Base-context issues are present. Repair the shared docs or index before trusting this context bundle.',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const ratio = bundle.total_source_chars / Math.max(bundle.max_chars, 1);
|
|
16
|
+
if (bundle.truncated && ratio >= 1.5) {
|
|
17
|
+
return {
|
|
18
|
+
status: 'critical',
|
|
19
|
+
recommendation: 'The current context is far above the bundle budget. Split the work, pause safely, or build a smaller bundle.',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (bundle.truncated || ratio >= 1.0) {
|
|
23
|
+
return {
|
|
24
|
+
status: 'warn',
|
|
25
|
+
recommendation: 'The current context is approaching the bundle budget. Narrow the query or checkpoint the work soon.',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
status: 'ok',
|
|
30
|
+
recommendation: 'The current context fits safely inside the bundle budget. Continue working.',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function tokenize(value) {
|
|
34
|
+
return String(value || '')
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.split(/[^a-z0-9]+/)
|
|
37
|
+
.map((token) => token.trim())
|
|
38
|
+
.filter((token) => token.length >= 3);
|
|
39
|
+
}
|
|
40
|
+
function slugify(value) {
|
|
41
|
+
return String(value || '')
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
45
|
+
.replace(/-+/g, '-')
|
|
46
|
+
.replace(/^-|-$/g, '') || 'context';
|
|
47
|
+
}
|
|
48
|
+
async function readMaybe(path) {
|
|
49
|
+
try {
|
|
50
|
+
return await readFile(path, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function loadBaseContextItems(rootDir, query) {
|
|
57
|
+
const home = getGlobalContextHome();
|
|
58
|
+
const localIndexPath = join(rootDir, 'docs', 'base-context', 'index.md');
|
|
59
|
+
const candidates = [
|
|
60
|
+
{ indexPath: localIndexPath, indexRootDir: rootDir, kind: 'local' },
|
|
61
|
+
{ indexPath: home.indexPath, indexRootDir: home.root, kind: 'global' },
|
|
62
|
+
];
|
|
63
|
+
const candidateResults = [];
|
|
64
|
+
for (const candidate of candidates) {
|
|
65
|
+
const issues = [];
|
|
66
|
+
const rawIndex = await readMaybe(candidate.indexPath);
|
|
67
|
+
if (!rawIndex.trim()) {
|
|
68
|
+
candidateResults.push({
|
|
69
|
+
items: [],
|
|
70
|
+
issues: candidate.kind === 'global' ? [] : [`Base-context index is missing or unreadable: ${candidate.indexPath}`],
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const resolved = await resolveContextFromIndex({
|
|
75
|
+
rootDir,
|
|
76
|
+
indexPath: candidate.indexPath,
|
|
77
|
+
indexRootDir: candidate.indexRootDir,
|
|
78
|
+
query,
|
|
79
|
+
maxMatches: 5,
|
|
80
|
+
}).catch(() => ({
|
|
81
|
+
matches: [],
|
|
82
|
+
unresolved_paths: [],
|
|
83
|
+
}));
|
|
84
|
+
const directMatches = (resolved.matches ?? []).map((match) => ({
|
|
85
|
+
label: match.label,
|
|
86
|
+
path: match.path,
|
|
87
|
+
score: match.score,
|
|
88
|
+
content: match.content.trim(),
|
|
89
|
+
}));
|
|
90
|
+
if ((resolved.unresolved_paths ?? []).length > 0) {
|
|
91
|
+
for (const path of resolved.unresolved_paths ?? []) {
|
|
92
|
+
issues.push(`Base-context path could not be resolved: ${path}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (directMatches.length > 0) {
|
|
96
|
+
candidateResults.push({ items: directMatches, issues });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const queryTokens = tokenize(query);
|
|
100
|
+
const entries = parseIndexDocument(rawIndex);
|
|
101
|
+
const ranked = [];
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const content = await readMaybe(join(candidate.indexRootDir, entry.path));
|
|
104
|
+
if (!content.trim())
|
|
105
|
+
continue;
|
|
106
|
+
const haystack = new Set(tokenize(content));
|
|
107
|
+
const score = queryTokens.reduce((sum, token) => sum + (haystack.has(token) ? 1 : 0), 0);
|
|
108
|
+
if (score <= 0)
|
|
109
|
+
continue;
|
|
110
|
+
ranked.push({
|
|
111
|
+
label: entry.label,
|
|
112
|
+
path: entry.path,
|
|
113
|
+
score,
|
|
114
|
+
content: content.trim(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (ranked.length === 0 && candidate.kind === 'local') {
|
|
118
|
+
issues.push('No matching base-context documents were found for the current query.');
|
|
119
|
+
}
|
|
120
|
+
candidateResults.push({
|
|
121
|
+
items: ranked.sort((left, right) => (right.score ?? 0) - (left.score ?? 0)).slice(0, 5),
|
|
122
|
+
issues,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const firstUseful = candidateResults.find((result) => result.items.length > 0);
|
|
126
|
+
if (firstUseful)
|
|
127
|
+
return firstUseful;
|
|
128
|
+
return candidateResults[candidateResults.length - 1] ?? { items: [], issues: [] };
|
|
129
|
+
}
|
|
130
|
+
async function loadReviewedArtifactItems(topicDir) {
|
|
131
|
+
if (!topicDir)
|
|
132
|
+
return [];
|
|
133
|
+
const requestSummary = await readMaybe(join(topicDir, 'request-summary.md'));
|
|
134
|
+
const spec = await readMaybe(join(topicDir, 'spec.md'));
|
|
135
|
+
const plan = await readMaybe(join(topicDir, 'implementation-plan.md'));
|
|
136
|
+
const brainstorm = await readMaybe(join(topicDir, 'brainstorm.md'));
|
|
137
|
+
const items = [];
|
|
138
|
+
if (requestSummary.trim()) {
|
|
139
|
+
items.push({
|
|
140
|
+
label: 'Request Summary',
|
|
141
|
+
path: 'request-summary.md',
|
|
142
|
+
content: requestSummary.trim(),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (spec.trim()) {
|
|
146
|
+
items.push({
|
|
147
|
+
label: 'Reviewed Spec',
|
|
148
|
+
path: 'spec.md',
|
|
149
|
+
content: spec.trim(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (plan.trim()) {
|
|
153
|
+
items.push({
|
|
154
|
+
label: 'Implementation Plan',
|
|
155
|
+
path: 'implementation-plan.md',
|
|
156
|
+
content: plan.trim(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (brainstorm.trim()) {
|
|
160
|
+
items.push({
|
|
161
|
+
label: 'Brainstorm',
|
|
162
|
+
path: 'brainstorm.md',
|
|
163
|
+
content: brainstorm.trim(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return items;
|
|
167
|
+
}
|
|
168
|
+
function mapDecisionItems(matches) {
|
|
169
|
+
return matches.map((match) => ({
|
|
170
|
+
label: match.title,
|
|
171
|
+
path: match.source_doc,
|
|
172
|
+
score: match.score,
|
|
173
|
+
content: [
|
|
174
|
+
match.summary.trim(),
|
|
175
|
+
match.source_topic_summary?.trim() ? `Source topic summary: ${match.source_topic_summary.trim()}` : '',
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join('\n'),
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
function mapTopicRecallItems(matches) {
|
|
182
|
+
return matches.map((match) => ({
|
|
183
|
+
label: match.topic_slug,
|
|
184
|
+
score: match.score,
|
|
185
|
+
content: [match.summary.trim(), match.request.trim()].filter(Boolean).join('\n'),
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
function trimToBudget(text, budget) {
|
|
189
|
+
if (budget <= 0)
|
|
190
|
+
return '';
|
|
191
|
+
if (text.length <= budget)
|
|
192
|
+
return text;
|
|
193
|
+
if (budget <= 3)
|
|
194
|
+
return text.slice(0, budget);
|
|
195
|
+
return `${text.slice(0, budget - 3).trimEnd()}...`;
|
|
196
|
+
}
|
|
197
|
+
function renderSectionHeader(title) {
|
|
198
|
+
return `## ${title}\n\n`;
|
|
199
|
+
}
|
|
200
|
+
function renderItem(item) {
|
|
201
|
+
const header = item.path ? `### ${item.label} (${item.path})\n\n` : `### ${item.label}\n\n`;
|
|
202
|
+
return `${header}${item.content.trim()}\n\n`;
|
|
203
|
+
}
|
|
204
|
+
export async function buildContextBundle({ rootDir, topicDir, query, maxChars = 6000, }) {
|
|
205
|
+
const effectiveRoot = rootDir || (topicDir ? getRootDirFromTopicDir(topicDir) : '');
|
|
206
|
+
if (!effectiveRoot) {
|
|
207
|
+
throw new Error('rootDir or topicDir is required');
|
|
208
|
+
}
|
|
209
|
+
const [baseContext, reviewedItems, decisionItems, topicRecallItems] = await Promise.all([
|
|
210
|
+
loadBaseContextItems(effectiveRoot, query),
|
|
211
|
+
loadReviewedArtifactItems(topicDir),
|
|
212
|
+
searchDecisionMemory({
|
|
213
|
+
rootDir: effectiveRoot,
|
|
214
|
+
query,
|
|
215
|
+
limit: 3,
|
|
216
|
+
}).then(mapDecisionItems),
|
|
217
|
+
searchPastTopics({
|
|
218
|
+
rootDir: effectiveRoot,
|
|
219
|
+
query,
|
|
220
|
+
limit: 3,
|
|
221
|
+
}).then(mapTopicRecallItems),
|
|
222
|
+
]);
|
|
223
|
+
const plannedSections = [
|
|
224
|
+
{ kind: 'reviewed_artifacts', title: 'Reviewed Artifacts', items: reviewedItems },
|
|
225
|
+
{ kind: 'base_context', title: 'Base Context', items: baseContext.items },
|
|
226
|
+
{ kind: 'decision_memory', title: 'Decision Memory', items: decisionItems },
|
|
227
|
+
{ kind: 'topic_recall', title: 'Past Topic Recall', items: topicRecallItems },
|
|
228
|
+
];
|
|
229
|
+
let remaining = maxChars;
|
|
230
|
+
const sections = [];
|
|
231
|
+
let rendered = '';
|
|
232
|
+
const totalSourceChars = plannedSections
|
|
233
|
+
.flatMap((section) => section.items)
|
|
234
|
+
.reduce((sum, item) => sum + item.content.length, 0);
|
|
235
|
+
const issues = [...baseContext.issues];
|
|
236
|
+
if (issues.length > 0) {
|
|
237
|
+
const issuesBlock = ['## Context Issues', '', ...issues.map((issue) => `- ${issue}`), '', ''].join('\n');
|
|
238
|
+
rendered += trimToBudget(issuesBlock, remaining);
|
|
239
|
+
remaining = maxChars - rendered.length;
|
|
240
|
+
}
|
|
241
|
+
for (const section of plannedSections) {
|
|
242
|
+
if (remaining <= 0)
|
|
243
|
+
break;
|
|
244
|
+
const header = renderSectionHeader(section.title);
|
|
245
|
+
if (header.length > remaining)
|
|
246
|
+
break;
|
|
247
|
+
let sectionRendered = header;
|
|
248
|
+
let sectionRemaining = remaining - header.length;
|
|
249
|
+
const keptItems = [];
|
|
250
|
+
for (const item of section.items) {
|
|
251
|
+
if (sectionRemaining <= 0)
|
|
252
|
+
break;
|
|
253
|
+
const renderedItem = renderItem(item);
|
|
254
|
+
const trimmed = trimToBudget(renderedItem, sectionRemaining);
|
|
255
|
+
if (!trimmed.trim())
|
|
256
|
+
break;
|
|
257
|
+
sectionRendered += trimmed;
|
|
258
|
+
keptItems.push({
|
|
259
|
+
...item,
|
|
260
|
+
content: trimToBudget(item.content, Math.max(0, trimmed.length)),
|
|
261
|
+
});
|
|
262
|
+
sectionRemaining = maxChars - (rendered.length + sectionRendered).length;
|
|
263
|
+
}
|
|
264
|
+
if (keptItems.length === 0 && section.kind !== 'base_context') {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
rendered += sectionRendered;
|
|
268
|
+
remaining = maxChars - rendered.length;
|
|
269
|
+
sections.push({
|
|
270
|
+
...section,
|
|
271
|
+
items: keptItems,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
query,
|
|
276
|
+
max_chars: maxChars,
|
|
277
|
+
total_source_chars: totalSourceChars,
|
|
278
|
+
truncated: totalSourceChars > rendered.length,
|
|
279
|
+
issues,
|
|
280
|
+
sections,
|
|
281
|
+
rendered: rendered.slice(0, maxChars),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
export async function writeContextBundle({ rootDir, topicDir, query, maxChars = 6000, outputPath, workflowStep = 'general', }) {
|
|
285
|
+
const bundle = await buildContextBundle({
|
|
286
|
+
rootDir,
|
|
287
|
+
topicDir,
|
|
288
|
+
query,
|
|
289
|
+
maxChars,
|
|
290
|
+
});
|
|
291
|
+
const effectiveRoot = rootDir || (topicDir ? getRootDirFromTopicDir(topicDir) : '');
|
|
292
|
+
const targetPath = outputPath || join(effectiveRoot, '.ax', 'context-bundles', `${slugify(query)}--${slugify(workflowStep)}.md`);
|
|
293
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
294
|
+
await writeFile(targetPath, `# Shift AX Context Bundle\n\n- workflow_step: ${workflowStep}\n- query: ${query}\n- max_chars: ${bundle.max_chars}\n\n${bundle.rendered.trim()}\n`, 'utf8');
|
|
295
|
+
return {
|
|
296
|
+
bundle,
|
|
297
|
+
output_path: targetPath,
|
|
298
|
+
status: classifyContextBundle(bundle).status,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
const EXCLUDED_SEGMENTS = new Set(['base-context', 'verification', 'roadmap', '.ax']);
|
|
4
|
+
const INCLUDED_KEYWORDS = [
|
|
5
|
+
'architecture',
|
|
6
|
+
'policy',
|
|
7
|
+
'policies',
|
|
8
|
+
'domain',
|
|
9
|
+
'glossary',
|
|
10
|
+
'context',
|
|
11
|
+
'overview',
|
|
12
|
+
'service',
|
|
13
|
+
'reference',
|
|
14
|
+
'decision',
|
|
15
|
+
'adr',
|
|
16
|
+
];
|
|
17
|
+
async function walkMarkdownFiles(dir, rootDir, results) {
|
|
18
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const fullPath = join(dir, entry.name);
|
|
21
|
+
const relPath = relative(rootDir, fullPath).replace(/\\/g, '/');
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
if ([...EXCLUDED_SEGMENTS].some((segment) => relPath.split('/').includes(segment))) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
await walkMarkdownFiles(fullPath, rootDir, results);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!entry.isFile())
|
|
30
|
+
continue;
|
|
31
|
+
if (!entry.name.toLowerCase().endsWith('.md'))
|
|
32
|
+
continue;
|
|
33
|
+
if (entry.name.toLowerCase() === 'index.md' && relPath.includes('docs/base-context'))
|
|
34
|
+
continue;
|
|
35
|
+
results.push(relPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function humanizeSlug(slug) {
|
|
39
|
+
return slug
|
|
40
|
+
.replace(/[-_]+/g, ' ')
|
|
41
|
+
.replace(/\b\w/g, (ch) => ch.toUpperCase())
|
|
42
|
+
.trim();
|
|
43
|
+
}
|
|
44
|
+
async function readHeading(rootDir, relPath) {
|
|
45
|
+
try {
|
|
46
|
+
const content = await readFile(join(rootDir, relPath), 'utf8');
|
|
47
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
48
|
+
return match ? match[1].trim() : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function looksRelevant(path) {
|
|
55
|
+
const normalized = path.toLowerCase();
|
|
56
|
+
return INCLUDED_KEYWORDS.some((keyword) => normalized.includes(keyword));
|
|
57
|
+
}
|
|
58
|
+
export async function discoverBaseContextEntries({ rootDir, }) {
|
|
59
|
+
const docsRoot = join(rootDir, 'docs');
|
|
60
|
+
const files = [];
|
|
61
|
+
await walkMarkdownFiles(docsRoot, rootDir, files).catch(() => undefined);
|
|
62
|
+
const relevantFiles = files.filter(looksRelevant);
|
|
63
|
+
const discovered = [];
|
|
64
|
+
for (const relPath of relevantFiles) {
|
|
65
|
+
const label = (await readHeading(rootDir, relPath)) ??
|
|
66
|
+
humanizeSlug(relPath.split('/').pop()?.replace(/\.md$/i, '') ?? relPath);
|
|
67
|
+
const topLevel = relPath.split('/')[1] ?? 'docs';
|
|
68
|
+
discovered.push({
|
|
69
|
+
label,
|
|
70
|
+
path: relPath,
|
|
71
|
+
reason: `Detected from docs/${topLevel}/ markdown structure.`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
return discovered.filter((entry) => {
|
|
76
|
+
const key = `${entry.label}::${entry.path}`;
|
|
77
|
+
if (seen.has(key))
|
|
78
|
+
return false;
|
|
79
|
+
seen.add(key);
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { getGlobalContextHome } from '../settings/global-context-home.js';
|
|
4
|
+
function slugify(value) {
|
|
5
|
+
return String(value || '')
|
|
6
|
+
.trim()
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
9
|
+
.replace(/-+/g, '-')
|
|
10
|
+
.replace(/^-|-$/g, '') || 'context';
|
|
11
|
+
}
|
|
12
|
+
async function writeMarkdown(path, content) {
|
|
13
|
+
await mkdir(dirname(path), { recursive: true });
|
|
14
|
+
await writeFile(path, `${content.trimEnd()}\n`, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
function renderProcedurePage({ workType, repository, }) {
|
|
17
|
+
const repoTitle = repository.repository;
|
|
18
|
+
const lines = [
|
|
19
|
+
`# ${workType} — ${repoTitle}`,
|
|
20
|
+
'',
|
|
21
|
+
'## Repository',
|
|
22
|
+
'',
|
|
23
|
+
`- Name: ${repoTitle}`,
|
|
24
|
+
...(repository.repositoryPath ? [`- Path: ${repository.repositoryPath}`] : []),
|
|
25
|
+
...(repository.purpose ? [`- Purpose: ${repository.purpose}`] : []),
|
|
26
|
+
'',
|
|
27
|
+
'## Working Directories',
|
|
28
|
+
'',
|
|
29
|
+
...(repository.directories.length > 0
|
|
30
|
+
? repository.directories.map((directory) => `- ${directory}`)
|
|
31
|
+
: ['- No directories were recorded yet.']),
|
|
32
|
+
'',
|
|
33
|
+
'## Working Method',
|
|
34
|
+
'',
|
|
35
|
+
repository.workflow.trim(),
|
|
36
|
+
'',
|
|
37
|
+
];
|
|
38
|
+
if ((repository.inferredNotes ?? []).length > 0) {
|
|
39
|
+
lines.push('## Inferred Workflow Notes', '');
|
|
40
|
+
lines.push(...(repository.inferredNotes ?? []).map((note) => `- ${note}`));
|
|
41
|
+
lines.push('');
|
|
42
|
+
}
|
|
43
|
+
if (repository.confirmationNotes?.trim()) {
|
|
44
|
+
lines.push('## Confirmation Notes', '', repository.confirmationNotes.trim(), '');
|
|
45
|
+
}
|
|
46
|
+
lines.push('## Volatility', '', repository.volatility === 'stable'
|
|
47
|
+
? 'This workflow is treated as stable cross-repo guidance.'
|
|
48
|
+
: 'This workflow is treated as volatile inspect-and-confirm guidance.', '');
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
function renderRepositoryPage({ repository, workTypes, }) {
|
|
52
|
+
const lines = [
|
|
53
|
+
`# ${repository.repository}`,
|
|
54
|
+
'',
|
|
55
|
+
'## Work Types',
|
|
56
|
+
'',
|
|
57
|
+
...workTypes.map((workType) => `- ${workType}`),
|
|
58
|
+
'',
|
|
59
|
+
];
|
|
60
|
+
if (repository.repositoryPath) {
|
|
61
|
+
lines.push('## Repository Path', '', repository.repositoryPath, '');
|
|
62
|
+
}
|
|
63
|
+
if (repository.purpose?.trim()) {
|
|
64
|
+
lines.push('## Purpose', '', repository.purpose.trim(), '');
|
|
65
|
+
}
|
|
66
|
+
lines.push('## Notes', '', 'This page is a repository index. Detailed working methods live in linked procedure pages.', '');
|
|
67
|
+
return lines.join('\n');
|
|
68
|
+
}
|
|
69
|
+
function renderWorkTypePage({ workType, procedureLinks, }) {
|
|
70
|
+
return [
|
|
71
|
+
`# ${workType.name}`,
|
|
72
|
+
'',
|
|
73
|
+
'## Summary',
|
|
74
|
+
'',
|
|
75
|
+
workType.summary?.trim() || 'No summary recorded yet.',
|
|
76
|
+
'',
|
|
77
|
+
'## Related Repositories',
|
|
78
|
+
'',
|
|
79
|
+
...procedureLinks.map((item) => `- ${item.repository} -> ${item.path}`),
|
|
80
|
+
'',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
function renderDomainLanguagePage(entry) {
|
|
84
|
+
return [
|
|
85
|
+
`# ${entry.term}`,
|
|
86
|
+
'',
|
|
87
|
+
entry.definition.trim(),
|
|
88
|
+
'',
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
|
91
|
+
function renderIndex({ primaryRoleSummary, workTypeEntries, domainEntries, }) {
|
|
92
|
+
return [
|
|
93
|
+
'# Shift AX Global Index',
|
|
94
|
+
'',
|
|
95
|
+
'## Primary Role',
|
|
96
|
+
'',
|
|
97
|
+
primaryRoleSummary.trim(),
|
|
98
|
+
'',
|
|
99
|
+
'## Work Types',
|
|
100
|
+
'',
|
|
101
|
+
...(workTypeEntries.length > 0
|
|
102
|
+
? workTypeEntries.map((entry) => `- ${entry.label} -> ${entry.path}`)
|
|
103
|
+
: ['- None yet.']),
|
|
104
|
+
'',
|
|
105
|
+
'## Domain Language',
|
|
106
|
+
'',
|
|
107
|
+
...(domainEntries.length > 0
|
|
108
|
+
? domainEntries.map((entry) => `- ${entry.label} -> ${entry.path}`)
|
|
109
|
+
: ['- None yet.']),
|
|
110
|
+
'',
|
|
111
|
+
'Notes:',
|
|
112
|
+
'',
|
|
113
|
+
'- The index stays intentionally lightweight.',
|
|
114
|
+
'- Detailed procedures and repository notes live in linked markdown pages.',
|
|
115
|
+
'- Reviewed topic artifacts override repository evidence, which override global knowledge, when they conflict.',
|
|
116
|
+
'',
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
119
|
+
export async function authorGlobalKnowledgeBase({ primaryRoleSummary, workTypes, domainLanguage, }) {
|
|
120
|
+
const home = getGlobalContextHome();
|
|
121
|
+
const contextDocs = [];
|
|
122
|
+
const workTypeEntries = [];
|
|
123
|
+
const domainEntries = [];
|
|
124
|
+
const repoWorkTypeMap = new Map();
|
|
125
|
+
for (const workType of workTypes) {
|
|
126
|
+
const workTypeSlug = slugify(workType.name);
|
|
127
|
+
const procedureLinks = [];
|
|
128
|
+
for (const repository of workType.repositories) {
|
|
129
|
+
const repoSlug = slugify(repository.repository || basename(repository.repositoryPath || 'repo'));
|
|
130
|
+
const procedureFileName = `${workTypeSlug}--${repoSlug}.md`;
|
|
131
|
+
const procedureAbsolutePath = join(home.proceduresDir, procedureFileName);
|
|
132
|
+
const procedureRelativePath = `procedures/${procedureFileName}`;
|
|
133
|
+
await writeMarkdown(procedureAbsolutePath, renderProcedurePage({
|
|
134
|
+
workType: workType.name,
|
|
135
|
+
repository,
|
|
136
|
+
}));
|
|
137
|
+
procedureLinks.push({
|
|
138
|
+
repository: repository.repository,
|
|
139
|
+
path: procedureRelativePath,
|
|
140
|
+
});
|
|
141
|
+
contextDocs.push({
|
|
142
|
+
label: `${workType.name} — ${repository.repository}`,
|
|
143
|
+
path: procedureRelativePath,
|
|
144
|
+
});
|
|
145
|
+
const repoKey = repoSlug;
|
|
146
|
+
if (!repoWorkTypeMap.has(repoKey)) {
|
|
147
|
+
repoWorkTypeMap.set(repoKey, new Set());
|
|
148
|
+
}
|
|
149
|
+
repoWorkTypeMap.get(repoKey).add(workType.name);
|
|
150
|
+
const repoAbsolutePath = join(home.reposDir, `${repoSlug}.md`);
|
|
151
|
+
const repoRelativePath = `repos/${repoSlug}.md`;
|
|
152
|
+
await writeMarkdown(repoAbsolutePath, renderRepositoryPage({
|
|
153
|
+
repository,
|
|
154
|
+
workTypes: [...repoWorkTypeMap.get(repoKey)],
|
|
155
|
+
}));
|
|
156
|
+
if (!contextDocs.some((entry) => entry.path === repoRelativePath)) {
|
|
157
|
+
contextDocs.push({
|
|
158
|
+
label: repository.repository,
|
|
159
|
+
path: repoRelativePath,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const workTypeRelativePath = `work-types/${workTypeSlug}.md`;
|
|
164
|
+
await writeMarkdown(join(home.workTypesDir, `${workTypeSlug}.md`), renderWorkTypePage({
|
|
165
|
+
workType,
|
|
166
|
+
procedureLinks,
|
|
167
|
+
}));
|
|
168
|
+
workTypeEntries.push({
|
|
169
|
+
label: workType.name,
|
|
170
|
+
path: workTypeRelativePath,
|
|
171
|
+
});
|
|
172
|
+
contextDocs.push({
|
|
173
|
+
label: workType.name,
|
|
174
|
+
path: workTypeRelativePath,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
for (const entry of domainLanguage) {
|
|
178
|
+
const slug = slugify(entry.term);
|
|
179
|
+
const relativePath = `domain-language/${slug}.md`;
|
|
180
|
+
await writeMarkdown(join(home.domainLanguageDir, `${slug}.md`), renderDomainLanguagePage(entry));
|
|
181
|
+
domainEntries.push({
|
|
182
|
+
label: entry.term,
|
|
183
|
+
path: relativePath,
|
|
184
|
+
});
|
|
185
|
+
contextDocs.push({
|
|
186
|
+
label: entry.term,
|
|
187
|
+
path: relativePath,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
await writeMarkdown(home.indexPath, renderIndex({
|
|
191
|
+
primaryRoleSummary,
|
|
192
|
+
workTypeEntries,
|
|
193
|
+
domainEntries,
|
|
194
|
+
}));
|
|
195
|
+
return {
|
|
196
|
+
indexPath: home.indexPath,
|
|
197
|
+
contextDocs,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { parseIndexDocument } from './index-resolver.js';
|
|
4
|
+
import { getGlobalContextHome } from '../settings/global-context-home.js';
|
|
5
|
+
const ELIGIBLE_GLOBAL_UPDATE_KINDS = new Set([
|
|
6
|
+
'work-type',
|
|
7
|
+
'domain-language',
|
|
8
|
+
]);
|
|
9
|
+
function slugify(value) {
|
|
10
|
+
return String(value || '')
|
|
11
|
+
.trim()
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
14
|
+
.replace(/-+/g, '-')
|
|
15
|
+
.replace(/^-|-$/g, '') || 'context';
|
|
16
|
+
}
|
|
17
|
+
function timestampPrefix(now = new Date()) {
|
|
18
|
+
return now.toISOString().replace(/[:.]/g, '-');
|
|
19
|
+
}
|
|
20
|
+
async function backupIfPresent(path) {
|
|
21
|
+
const home = getGlobalContextHome();
|
|
22
|
+
const current = await readFile(path, 'utf8').catch(() => '');
|
|
23
|
+
if (!current)
|
|
24
|
+
return;
|
|
25
|
+
await mkdir(home.backupsDir, { recursive: true });
|
|
26
|
+
await writeFile(join(home.backupsDir, `${timestampPrefix()}-${basename(path)}`), current, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
async function upsertIndexEntry(entry) {
|
|
29
|
+
const home = getGlobalContextHome();
|
|
30
|
+
const raw = await readFile(home.indexPath, 'utf8').catch(() => '# Shift AX Global Index\n\n## Work Types\n\n- None yet.\n\n## Domain Language\n\n- None yet.\n');
|
|
31
|
+
const entries = parseIndexDocument(raw).filter((current) => current.label !== entry.label);
|
|
32
|
+
entries.push(entry);
|
|
33
|
+
const workTypeEntries = entries.filter((current) => current.path.startsWith('work-types/'));
|
|
34
|
+
const domainEntries = entries.filter((current) => current.path.startsWith('domain-language/'));
|
|
35
|
+
const rendered = [
|
|
36
|
+
'# Shift AX Global Index',
|
|
37
|
+
'',
|
|
38
|
+
'## Work Types',
|
|
39
|
+
'',
|
|
40
|
+
...(workTypeEntries.length > 0 ? workTypeEntries.map((item) => `- ${item.label} -> ${item.path}`) : ['- None yet.']),
|
|
41
|
+
'',
|
|
42
|
+
'## Domain Language',
|
|
43
|
+
'',
|
|
44
|
+
...(domainEntries.length > 0 ? domainEntries.map((item) => `- ${item.label} -> ${item.path}`) : ['- None yet.']),
|
|
45
|
+
'',
|
|
46
|
+
].join('\n');
|
|
47
|
+
await backupIfPresent(home.indexPath);
|
|
48
|
+
await mkdir(home.root, { recursive: true });
|
|
49
|
+
await writeFile(home.indexPath, `${rendered.trimEnd()}\n`, 'utf8');
|
|
50
|
+
}
|
|
51
|
+
function parseGlobalKnowledgeUpdates(markdown, source) {
|
|
52
|
+
const lines = String(markdown || '').split(/\r?\n/);
|
|
53
|
+
const updates = [];
|
|
54
|
+
let inSection = false;
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (/^##\s+Global Knowledge Updates$/i.test(trimmed)) {
|
|
58
|
+
inSection = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (inSection && /^##\s+/.test(trimmed)) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (!inSection || !trimmed.startsWith('-'))
|
|
65
|
+
continue;
|
|
66
|
+
const body = trimmed.slice(1).trim();
|
|
67
|
+
if (/^none/i.test(body))
|
|
68
|
+
continue;
|
|
69
|
+
const match = body.match(/^(work-type|domain-language):\s*(.+?)\s*->\s*(.+)$/i);
|
|
70
|
+
if (!match)
|
|
71
|
+
continue;
|
|
72
|
+
updates.push({
|
|
73
|
+
kind: match[1].toLowerCase(),
|
|
74
|
+
label: match[2].trim(),
|
|
75
|
+
content: match[3].trim(),
|
|
76
|
+
source,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return updates;
|
|
80
|
+
}
|
|
81
|
+
export async function applyGlobalKnowledgeUpdatesFromArtifacts({ brainstormContent, specContent, implementationPlanContent, }) {
|
|
82
|
+
const updates = [
|
|
83
|
+
...parseGlobalKnowledgeUpdates(brainstormContent ?? '', 'brainstorm.md'),
|
|
84
|
+
...parseGlobalKnowledgeUpdates(specContent ?? '', 'spec.md'),
|
|
85
|
+
...parseGlobalKnowledgeUpdates(implementationPlanContent ?? '', 'implementation-plan.md'),
|
|
86
|
+
];
|
|
87
|
+
const unique = new Map();
|
|
88
|
+
for (const update of updates) {
|
|
89
|
+
unique.set(`${update.kind}:${update.label}`, update);
|
|
90
|
+
}
|
|
91
|
+
const home = getGlobalContextHome();
|
|
92
|
+
const applied = [...unique.values()];
|
|
93
|
+
for (const update of applied) {
|
|
94
|
+
if (!ELIGIBLE_GLOBAL_UPDATE_KINDS.has(update.kind)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const slug = slugify(update.label);
|
|
98
|
+
const relativePath = update.kind === 'work-type' ? `work-types/${slug}.md` : `domain-language/${slug}.md`;
|
|
99
|
+
const absolutePath = update.kind === 'work-type'
|
|
100
|
+
? join(home.workTypesDir, `${slug}.md`)
|
|
101
|
+
: join(home.domainLanguageDir, `${slug}.md`);
|
|
102
|
+
const markdown = update.kind === 'work-type'
|
|
103
|
+
? `# ${update.label}\n\n## Stable Guidance\n\n${update.content}\n\n## Source\n\n- ${update.source}\n`
|
|
104
|
+
: `# ${update.label}\n\n${update.content}\n\n## Source\n\n- ${update.source}\n`;
|
|
105
|
+
await backupIfPresent(absolutePath);
|
|
106
|
+
await mkdir(update.kind === 'work-type' ? home.workTypesDir : home.domainLanguageDir, {
|
|
107
|
+
recursive: true,
|
|
108
|
+
});
|
|
109
|
+
await writeFile(absolutePath, markdown, 'utf8');
|
|
110
|
+
await upsertIndexEntry({
|
|
111
|
+
label: update.label,
|
|
112
|
+
path: relativePath,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return applied;
|
|
116
|
+
}
|