@aabadin/project-memory-context 0.1.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 +674 -0
- package/README.md +123 -0
- package/bin/pmc.mjs +11 -0
- package/cli/apply-enrichment-result.mjs +31 -0
- package/cli/batch-enrich.mjs +5 -0
- package/cli/bootstrap.mjs +357 -0
- package/cli/build-worklist.mjs +35 -0
- package/cli/context.mjs +49 -0
- package/cli/doctor.mjs +29 -0
- package/cli/enrich-batch.mjs +11 -0
- package/cli/enrich-loop.sh +117 -0
- package/cli/enrich-orchestrator.mjs +5 -0
- package/cli/enrich-queue.mjs +525 -0
- package/cli/enrich-sync.mjs +5 -0
- package/cli/enrich.mjs +51 -0
- package/cli/fail-enrichment.mjs +28 -0
- package/cli/finalize-enrichment.mjs +25 -0
- package/cli/init.mjs +66 -0
- package/cli/install-pmc.mjs +153 -0
- package/cli/materialize-enrichment-artifacts.mjs +38 -0
- package/cli/new-project.mjs +41 -0
- package/cli/prepare-semantic-jobs.mjs +8 -0
- package/cli/project-context.mjs +224 -0
- package/cli/sanitize.mjs +235 -0
- package/cli/save-intake-context.mjs +22 -0
- package/cli/setup.mjs +80 -0
- package/cli/status.mjs +81 -0
- package/mcp/local-model-server.mjs +74 -0
- package/package.json +60 -0
- package/plugin/index.mjs +27 -0
- package/src/artifacts.mjs +39 -0
- package/src/change-detector.mjs +10 -0
- package/src/command-dispatch.mjs +84 -0
- package/src/declared-intake.mjs +25 -0
- package/src/doctor.mjs +114 -0
- package/src/enrichment-artifacts.mjs +67 -0
- package/src/enrichment-attempts.mjs +17 -0
- package/src/enrichment-config.mjs +121 -0
- package/src/enrichment-driver.mjs +167 -0
- package/src/enrichment-errors.mjs +46 -0
- package/src/enrichment-linker.mjs +29 -0
- package/src/extractors/architecture-extractor.mjs +8 -0
- package/src/extractors/js-ts-extractor.mjs +118 -0
- package/src/extractors/regex-extractor.mjs +439 -0
- package/src/extractors/rules-extractor.mjs +9 -0
- package/src/extractors/stack-extractor.mjs +48 -0
- package/src/extractors/structure-extractor.mjs +31 -0
- package/src/fail-enrichment.mjs +33 -0
- package/src/finalize-enrichment.mjs +30 -0
- package/src/graph-backfill.mjs +35 -0
- package/src/graph-node-resolver.mjs +64 -0
- package/src/index.mjs +2 -0
- package/src/intake-context.mjs +16 -0
- package/src/invalidation-matrix.mjs +33 -0
- package/src/markdown-renderer.mjs +27 -0
- package/src/materializer.mjs +128 -0
- package/src/memory-payload.mjs +55 -0
- package/src/persist-enrichment-result.mjs +33 -0
- package/src/platform.mjs +111 -0
- package/src/plugin-config.mjs +17 -0
- package/src/prepare-semantic-jobs.mjs +33 -0
- package/src/project-context-schema.mjs +57 -0
- package/src/providers/cloud-api-provider.mjs +88 -0
- package/src/providers/local-model-provider.mjs +67 -0
- package/src/refresh-state.mjs +21 -0
- package/src/result-input.mjs +9 -0
- package/src/retrieval/context-renderer.mjs +97 -0
- package/src/retrieval/query-engine.mjs +230 -0
- package/src/semantic-report.mjs +26 -0
- package/src/semantic-unit.mjs +74 -0
- package/src/setup-bootstrap.mjs +131 -0
- package/src/symbol-extractor.mjs +29 -0
- package/src/symbol-index.mjs +30 -0
- package/src/symbol-keys.mjs +28 -0
- package/src/sync-manifest.mjs +119 -0
- package/src/template-installer.mjs +181 -0
- package/src/worklist-state.mjs +12 -0
- package/templates/claude-code/CLAUDE.md.snippet +36 -0
- package/templates/cursor/.cursorrules.snippet +36 -0
- package/templates/generic/README-SETUP.md +53 -0
- package/templates/opencode/agent/enrich.md +28 -0
- package/templates/opencode/autostart-snippet.md +13 -0
- package/templates/opencode/commands/get-context.md +22 -0
- package/templates/opencode/commands/new-project.md +32 -0
- package/templates/opencode/commands/sanitize.md +21 -0
- package/templates/opencode/commands/sync-context.md +22 -0
- package/templates/project-memory-context workflow.md +129 -0
- package/templates/project-memory-context.md +42 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { recordEnrichmentFailure } from '../src/fail-enrichment.mjs';
|
|
5
|
+
|
|
6
|
+
const [, , symbolKey, errorMessage, failedAtArg] = process.argv;
|
|
7
|
+
|
|
8
|
+
if (!symbolKey || !errorMessage) {
|
|
9
|
+
console.error('Usage: node fail-enrichment.mjs <symbol-key> <error-message> [failed-at]');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const projectRoot = resolve(process.cwd());
|
|
14
|
+
const failedAt = failedAtArg ?? new Date().toISOString();
|
|
15
|
+
const failed = await recordEnrichmentFailure({
|
|
16
|
+
projectRoot,
|
|
17
|
+
symbolKey,
|
|
18
|
+
error: errorMessage,
|
|
19
|
+
failedAt,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
console.log(JSON.stringify({
|
|
23
|
+
saved: true,
|
|
24
|
+
worklistFile: failed.worklistFile,
|
|
25
|
+
failuresFile: failed.failuresFile,
|
|
26
|
+
symbolKey,
|
|
27
|
+
failedAt,
|
|
28
|
+
}, null, 2));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { finalizeEnrichment } from '../src/finalize-enrichment.mjs';
|
|
5
|
+
import { loadResultInput } from '../src/result-input.mjs';
|
|
6
|
+
|
|
7
|
+
const [, , rawResultInput] = process.argv;
|
|
8
|
+
|
|
9
|
+
if (!rawResultInput) {
|
|
10
|
+
console.error('Usage: node finalize-enrichment.mjs <result-json|@result-file>');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const projectRoot = resolve(process.cwd());
|
|
15
|
+
const result = await loadResultInput(rawResultInput);
|
|
16
|
+
const finalized = await finalizeEnrichment({ projectRoot, result });
|
|
17
|
+
|
|
18
|
+
console.log(JSON.stringify({
|
|
19
|
+
saved: true,
|
|
20
|
+
graphFile: finalized.graphFile,
|
|
21
|
+
indexFile: finalized.indexFile,
|
|
22
|
+
worklistFile: finalized.worklistFile,
|
|
23
|
+
symbolKey: result.symbolKey,
|
|
24
|
+
status: result.status,
|
|
25
|
+
}, null, 2));
|
package/cli/init.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { resolveConfigDirs } from '../src/platform.mjs';
|
|
6
|
+
import { detectAgentType, installAgentTemplates } from '../src/template-installer.mjs';
|
|
7
|
+
|
|
8
|
+
function parseArgs(args) {
|
|
9
|
+
const result = { agent: null };
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
if (args[i] === '--agent' && args[i + 1]) {
|
|
12
|
+
result.agent = args[++i];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function printHelp() {
|
|
19
|
+
console.log(`Usage: pmc init [--agent opencode|claude-code|cursor|generic]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--agent <type> Agent type to initialize. Auto-detected if omitted.
|
|
23
|
+
|
|
24
|
+
Supported agents:
|
|
25
|
+
opencode OpenCode (commands + agents + AGENTS.md autostart)
|
|
26
|
+
claude-code Claude Code (CLAUDE.md instructions)
|
|
27
|
+
cursor Cursor (.cursorrules instructions)
|
|
28
|
+
generic Generic CLI-only (README-SETUP.md)`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function main(args = process.argv.slice(2)) {
|
|
32
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
33
|
+
printHelp();
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const projectRoot = resolve(process.cwd());
|
|
38
|
+
const { agent: requestedAgent } = parseArgs(args);
|
|
39
|
+
const agent = requestedAgent || detectAgentType(projectRoot);
|
|
40
|
+
|
|
41
|
+
if (requestedAgent === null) {
|
|
42
|
+
console.error(`[pmc:init] Auto-detected agent: ${agent}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { globalConfig } = resolveConfigDirs(projectRoot);
|
|
46
|
+
|
|
47
|
+
await installAgentTemplates({
|
|
48
|
+
projectRoot,
|
|
49
|
+
agent,
|
|
50
|
+
globalConfigDir: agent === 'opencode' ? globalConfig : undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.error(`[pmc:init] Installed PMC templates for ${agent}`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
58
|
+
const exitCode = await main().catch((error) => {
|
|
59
|
+
console.error('[pmc:init] FATAL:', error.message);
|
|
60
|
+
return 1;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (exitCode !== 0) {
|
|
64
|
+
process.exit(exitCode);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve, dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
readdirSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
copyFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const DEFAULT_SOURCE_ROOT = resolve(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
function log(msg) {
|
|
16
|
+
console.error(`[install-pmc] ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function copyMjsTree(srcDir, dstDir, opts = {}) {
|
|
20
|
+
const { skipPatterns = [] } = opts;
|
|
21
|
+
mkdirSync(dstDir, { recursive: true });
|
|
22
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
23
|
+
let copied = 0;
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.name === 'node_modules' || entry.name === 'db') continue;
|
|
27
|
+
if (entry.name.endsWith('.test.mjs')) continue;
|
|
28
|
+
if (skipPatterns.some(p => entry.name.includes(p))) continue;
|
|
29
|
+
|
|
30
|
+
const srcPath = join(srcDir, entry.name);
|
|
31
|
+
const dstPath = join(dstDir, entry.name);
|
|
32
|
+
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
copied += copyMjsTree(srcPath, dstPath, opts);
|
|
35
|
+
} else if (entry.isFile() && entry.name.endsWith('.mjs')) {
|
|
36
|
+
copyFileSync(srcPath, dstPath);
|
|
37
|
+
copied++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return copied;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function copyTemplatesDir(srcTemplates, dstTemplates) {
|
|
45
|
+
if (!existsSync(srcTemplates)) return 0;
|
|
46
|
+
mkdirSync(dstTemplates, { recursive: true });
|
|
47
|
+
const entries = readdirSync(srcTemplates, { withFileTypes: true });
|
|
48
|
+
let copied = 0;
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isFile()) continue;
|
|
52
|
+
copyFileSync(join(srcTemplates, entry.name), join(dstTemplates, entry.name));
|
|
53
|
+
copied++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return copied;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function installPmcTools({ sourceRoot, targetRoot }) {
|
|
60
|
+
const srcCli = resolve(sourceRoot, 'cli');
|
|
61
|
+
const srcSrc = resolve(sourceRoot, 'src');
|
|
62
|
+
const srcTemplates = resolve(sourceRoot, 'templates');
|
|
63
|
+
|
|
64
|
+
const dstBase = resolve(targetRoot, 'tools', 'project-memory-context');
|
|
65
|
+
const dstCli = resolve(dstBase, 'cli');
|
|
66
|
+
const dstSrc = resolve(dstBase, 'src');
|
|
67
|
+
const dstTemplates = resolve(dstBase, 'templates');
|
|
68
|
+
|
|
69
|
+
mkdirSync(dstBase, { recursive: true });
|
|
70
|
+
mkdirSync(dstCli, { recursive: true });
|
|
71
|
+
|
|
72
|
+
let cliFiles = 0;
|
|
73
|
+
let srcFiles = 0;
|
|
74
|
+
let templateFiles = 0;
|
|
75
|
+
|
|
76
|
+
const cliEntries = readdirSync(srcCli, { withFileTypes: true });
|
|
77
|
+
for (const entry of cliEntries) {
|
|
78
|
+
if (!entry.isFile() || !entry.name.endsWith('.mjs')) continue;
|
|
79
|
+
if (entry.name.endsWith('.test.mjs')) continue;
|
|
80
|
+
copyFileSync(join(srcCli, entry.name), join(dstCli, entry.name));
|
|
81
|
+
cliFiles++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (existsSync(srcSrc)) {
|
|
85
|
+
srcFiles = copyMjsTree(srcSrc, dstSrc);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
templateFiles = copyTemplatesDir(srcTemplates, dstTemplates);
|
|
89
|
+
|
|
90
|
+
const planningBase = resolve(targetRoot, '.planning', 'project-memory-context');
|
|
91
|
+
for (const sub of ['intake', 'graph', 'enrichment', 'memory-db', 'db']) {
|
|
92
|
+
mkdirSync(resolve(planningBase, sub), { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const installState = {
|
|
96
|
+
installedAt: new Date().toISOString(),
|
|
97
|
+
sourceRoot: resolve(sourceRoot),
|
|
98
|
+
version: '0.1.0',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
writeFileSync(
|
|
102
|
+
resolve(planningBase, 'install.json'),
|
|
103
|
+
`${JSON.stringify(installState, null, 2)}\n`,
|
|
104
|
+
'utf8'
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return { cliFiles, srcFiles, templateFiles };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
const args = process.argv.slice(2);
|
|
112
|
+
|
|
113
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
114
|
+
console.log(`
|
|
115
|
+
install-pmc - Copy PMC tools into a target project
|
|
116
|
+
|
|
117
|
+
Usage:
|
|
118
|
+
node install-pmc.mjs [target-project-dir]
|
|
119
|
+
|
|
120
|
+
Arguments:
|
|
121
|
+
target-project-dir Path to target project (default: current directory)
|
|
122
|
+
|
|
123
|
+
Options:
|
|
124
|
+
--help, -h Show this help
|
|
125
|
+
`);
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const targetArg = args.find(a => !a.startsWith('-'));
|
|
130
|
+
const targetRoot = targetArg ? resolve(targetArg) : process.cwd();
|
|
131
|
+
const sourceRoot = DEFAULT_SOURCE_ROOT;
|
|
132
|
+
|
|
133
|
+
if (!existsSync(targetRoot)) {
|
|
134
|
+
console.error(`[install-pmc] ERROR: Target directory not found: ${targetRoot}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
log(`Source: ${sourceRoot}`);
|
|
139
|
+
log(`Target: ${targetRoot}`);
|
|
140
|
+
|
|
141
|
+
const result = installPmcTools({ sourceRoot, targetRoot });
|
|
142
|
+
|
|
143
|
+
log(`Copied: ${result.cliFiles} CLI files, ${result.srcFiles} src files, ${result.templateFiles} templates`);
|
|
144
|
+
log('Created .planning/project-memory-context/ directory structure');
|
|
145
|
+
log('Done.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
149
|
+
main().catch(err => {
|
|
150
|
+
console.error('[install-pmc] FATAL:', err.message);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { readJsonArtifact } from '../src/artifacts.mjs';
|
|
5
|
+
import { persistEnrichmentArtifacts } from '../src/enrichment-artifacts.mjs';
|
|
6
|
+
import { loadResultInput } from '../src/result-input.mjs';
|
|
7
|
+
|
|
8
|
+
const [, , rawJobInput, rawReportInput, memoryIdArg, enrichedAtArg] = process.argv;
|
|
9
|
+
|
|
10
|
+
if (!rawJobInput || !rawReportInput) {
|
|
11
|
+
console.error('Usage: node materialize-enrichment-artifacts.mjs <job-json|@job-file> <report-json|@report-file> [memory-id] [enriched-at]');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const projectRoot = resolve(process.cwd());
|
|
16
|
+
const projectSlug = projectRoot.split(/[\\/]/).filter(Boolean).pop() ?? 'project';
|
|
17
|
+
const enrichedAt = enrichedAtArg ?? new Date().toISOString();
|
|
18
|
+
const memoryId = memoryIdArg && memoryIdArg !== '-' ? memoryIdArg : null;
|
|
19
|
+
|
|
20
|
+
const job = await loadResultInput(rawJobInput);
|
|
21
|
+
const report = await loadResultInput(rawReportInput);
|
|
22
|
+
|
|
23
|
+
const persisted = await persistEnrichmentArtifacts({
|
|
24
|
+
projectRoot,
|
|
25
|
+
projectSlug,
|
|
26
|
+
job,
|
|
27
|
+
report,
|
|
28
|
+
memoryId,
|
|
29
|
+
enrichedAt,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(JSON.stringify({
|
|
33
|
+
saved: true,
|
|
34
|
+
memoryPayloadFile: persisted.memoryPayloadFile,
|
|
35
|
+
enrichmentResultFile: persisted.enrichmentResultFile,
|
|
36
|
+
memoryId,
|
|
37
|
+
summary: persisted.semantic.summary,
|
|
38
|
+
}, null, 2));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const SCRIPT_PATH = resolve(dirname(fileURLToPath(import.meta.url)), 'bootstrap.mjs');
|
|
7
|
+
|
|
8
|
+
export { buildDefaultEnrichmentConfig, mergeEnrichmentConfig } from './bootstrap.mjs';
|
|
9
|
+
|
|
10
|
+
export async function main(args = process.argv.slice(2)) {
|
|
11
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
12
|
+
console.log(`
|
|
13
|
+
new-project - Bootstrap project-memory-context in any repo
|
|
14
|
+
(Delegates to bootstrap.mjs - the portable PMC installer)
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
node new-project.mjs <target-repo> [--stage-a] [--stage-b] [--all] [--enrich]
|
|
18
|
+
`);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
23
|
+
const child = spawn(process.execPath, [SCRIPT_PATH, ...args], { stdio: 'inherit' });
|
|
24
|
+
child.once('error', rejectPromise);
|
|
25
|
+
child.once('exit', (code, signal) => {
|
|
26
|
+
if (signal) {
|
|
27
|
+
rejectPromise(new Error(`new-project exited from signal ${signal}`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
resolvePromise(code ?? 0);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
36
|
+
const exitCode = await main().catch((err) => {
|
|
37
|
+
console.error('[new-project] FATAL:', err.message);
|
|
38
|
+
return 1;
|
|
39
|
+
});
|
|
40
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { prepareSemanticJobs } from '../src/prepare-semantic-jobs.mjs';
|
|
5
|
+
|
|
6
|
+
const projectRoot = resolve(process.cwd());
|
|
7
|
+
const result = await prepareSemanticJobs({ projectRoot });
|
|
8
|
+
console.log(JSON.stringify({ saved: true, file: result.file, count: result.count }, null, 2));
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { basename, join, resolve, dirname } from 'node:path';
|
|
3
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import { ensureProjectMemoryContextDirs, writeJsonArtifact, readJsonArtifact } from '../src/artifacts.mjs';
|
|
7
|
+
import { detectStackContext } from '../src/extractors/stack-extractor.mjs';
|
|
8
|
+
import { detectStructureContext } from '../src/extractors/structure-extractor.mjs';
|
|
9
|
+
import { detectArchitectureContext } from '../src/extractors/architecture-extractor.mjs';
|
|
10
|
+
import { detectRulesContext } from '../src/extractors/rules-extractor.mjs';
|
|
11
|
+
import { createDeclaredProjectContextTemplates } from '../src/declared-intake.mjs';
|
|
12
|
+
import { materializeProjectContextMemories } from '../src/materializer.mjs';
|
|
13
|
+
import { renderProjectContextMarkdown } from '../src/markdown-renderer.mjs';
|
|
14
|
+
import { detectChangedFilesFromHashes } from '../src/change-detector.mjs';
|
|
15
|
+
import { detectInvalidatedProjectContextKinds } from '../src/invalidation-matrix.mjs';
|
|
16
|
+
import { appendSyncEntries, createSyncEntry } from '../src/sync-manifest.mjs';
|
|
17
|
+
|
|
18
|
+
function log(message) {
|
|
19
|
+
console.error(`[project-context] ${message}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function readText(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
return await readFile(filePath, 'utf8');
|
|
25
|
+
} catch {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TRACKED_FILES = ['package.json', 'tsconfig.json', 'README.md'];
|
|
31
|
+
|
|
32
|
+
async function buildDetectedContext(projectRoot) {
|
|
33
|
+
const stack = await detectStackContext(projectRoot);
|
|
34
|
+
const structure = await detectStructureContext(projectRoot);
|
|
35
|
+
const architecture = await detectArchitectureContext({ graph: { nodes: structure.entryPoints.map((entry) => ({ label: entry })), edges: [] } });
|
|
36
|
+
const rules = await detectRulesContext({ readmeText: await readText(join(projectRoot, 'README.md')) });
|
|
37
|
+
return { stack, structure, architecture, rules };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildDeclaredContext() {
|
|
41
|
+
return {
|
|
42
|
+
architectureTarget: { architecture: '' },
|
|
43
|
+
technicalRules: { rules: [] },
|
|
44
|
+
projectRequirements: { requirements: [] },
|
|
45
|
+
knownIssuesAndFixes: { items: [] },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function writeMemories(dirs, memories) {
|
|
50
|
+
for (const memory of memories) {
|
|
51
|
+
const jsonName = `${memory.kind}.json`;
|
|
52
|
+
const markdownName = `${memory.kind.toUpperCase()}.md`;
|
|
53
|
+
await writeJsonArtifact(join(dirs.projectContextMaterialized, jsonName), memory);
|
|
54
|
+
const markdown = renderProjectContextMarkdown(memory);
|
|
55
|
+
await mkdir(dirname(join(dirs.projectContextMarkdown, markdownName)), { recursive: true });
|
|
56
|
+
await writeFile(join(dirs.projectContextMarkdown, markdownName), markdown, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function computeHashes(projectRoot, files) {
|
|
61
|
+
const hashes = {};
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
try {
|
|
64
|
+
const content = await readFile(join(projectRoot, file), 'utf8');
|
|
65
|
+
hashes[file] = createHash('sha256').update(content).digest('hex');
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
return hashes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runBootstrap(projectRoot, dirs) {
|
|
72
|
+
const updatedAt = new Date().toISOString();
|
|
73
|
+
const detected = await buildDetectedContext(projectRoot);
|
|
74
|
+
const declared = buildDeclaredContext();
|
|
75
|
+
|
|
76
|
+
const templates = createDeclaredProjectContextTemplates();
|
|
77
|
+
for (const [fileName, payload] of Object.entries(templates)) {
|
|
78
|
+
await writeJsonArtifact(join(dirs.projectContextDeclared, fileName), payload);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const memories = materializeProjectContextMemories({
|
|
82
|
+
projectSlug: basename(projectRoot).toLowerCase(),
|
|
83
|
+
detected,
|
|
84
|
+
declared,
|
|
85
|
+
updatedAt,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await writeMemories(dirs, memories);
|
|
89
|
+
|
|
90
|
+
const nextHashes = await computeHashes(projectRoot, TRACKED_FILES);
|
|
91
|
+
await writeJsonArtifact(join(dirs.projectContextState, 'content-hashes.json'), { hashes: nextHashes });
|
|
92
|
+
await writeJsonArtifact(join(dirs.projectContextState, 'last-run.json'), { updatedAt, count: memories.length });
|
|
93
|
+
|
|
94
|
+
const syncEntries = memories.map((memory) => createSyncEntry({
|
|
95
|
+
action: 'upsert',
|
|
96
|
+
keyTag: `key:${memory.memory_key}`,
|
|
97
|
+
content: `# ${memory.title}\n\n${memory.summary}\n\n${memory.body}`,
|
|
98
|
+
category: 'architecture',
|
|
99
|
+
tags: [...memory.tags, `key:${memory.memory_key}`],
|
|
100
|
+
source: 'project-context',
|
|
101
|
+
}));
|
|
102
|
+
await appendSyncEntries(dirs.enrichment, syncEntries);
|
|
103
|
+
|
|
104
|
+
log(`Wrote ${memories.length} project-context memories.`);
|
|
105
|
+
log(`Appended ${syncEntries.length} sync-manifest entries.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function runRefresh(projectRoot, dirs) {
|
|
109
|
+
const updatedAt = new Date().toISOString();
|
|
110
|
+
|
|
111
|
+
const nextHashes = await computeHashes(projectRoot, TRACKED_FILES);
|
|
112
|
+
const previousState = await readJsonArtifact(join(dirs.projectContextState, 'content-hashes.json'), { hashes: {} });
|
|
113
|
+
const changed = detectChangedFilesFromHashes(previousState.hashes, nextHashes);
|
|
114
|
+
const invalidatedKinds = detectInvalidatedProjectContextKinds(changed);
|
|
115
|
+
|
|
116
|
+
if (invalidatedKinds.length === 0) {
|
|
117
|
+
log('No changes detected. Nothing to refresh.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
log(`Changed files: ${changed.join(', ')}`);
|
|
122
|
+
log(`Invalidated kinds: ${invalidatedKinds.join(', ')}`);
|
|
123
|
+
|
|
124
|
+
const detected = await buildDetectedContext(projectRoot);
|
|
125
|
+
const declared = buildDeclaredContext();
|
|
126
|
+
|
|
127
|
+
const allMemories = materializeProjectContextMemories({
|
|
128
|
+
projectSlug: basename(projectRoot).toLowerCase(),
|
|
129
|
+
detected,
|
|
130
|
+
declared,
|
|
131
|
+
updatedAt,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const invalidatedSet = new Set(invalidatedKinds);
|
|
135
|
+
const toRewrite = allMemories.filter((m) => invalidatedSet.has(m.kind));
|
|
136
|
+
|
|
137
|
+
await writeMemories(dirs, toRewrite);
|
|
138
|
+
|
|
139
|
+
const syncEntries = toRewrite.map((memory) => createSyncEntry({
|
|
140
|
+
action: 'upsert',
|
|
141
|
+
keyTag: `key:${memory.memory_key}`,
|
|
142
|
+
content: `# ${memory.title}\n\n${memory.summary}\n\n${memory.body}`,
|
|
143
|
+
category: 'architecture',
|
|
144
|
+
tags: [...memory.tags, `key:${memory.memory_key}`],
|
|
145
|
+
source: 'project-context-refresh',
|
|
146
|
+
}));
|
|
147
|
+
await appendSyncEntries(dirs.enrichment, syncEntries);
|
|
148
|
+
|
|
149
|
+
const affectsArchitecture = invalidatedSet.has('architecture-current') || invalidatedSet.has('structure-summary');
|
|
150
|
+
if (affectsArchitecture) {
|
|
151
|
+
await markEnrichedSymbolsStale(dirs, basename(projectRoot).toLowerCase());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await writeJsonArtifact(join(dirs.projectContextState, 'content-hashes.json'), { hashes: nextHashes });
|
|
155
|
+
await writeJsonArtifact(join(dirs.projectContextState, 'last-run.json'), { updatedAt, count: toRewrite.length });
|
|
156
|
+
|
|
157
|
+
log(`Refreshed ${toRewrite.length} memories.`);
|
|
158
|
+
log(`Appended ${syncEntries.length} sync-manifest entries.`);
|
|
159
|
+
if (affectsArchitecture) {
|
|
160
|
+
log('Marked enriched symbols as stale (architecture/structure changed).');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function markEnrichedSymbolsStale(dirs, projectSlug) {
|
|
165
|
+
const worklistPath = join(dirs.enrichment, 'worklist.json');
|
|
166
|
+
let worklist;
|
|
167
|
+
try {
|
|
168
|
+
worklist = JSON.parse(await readFile(worklistPath, 'utf8'));
|
|
169
|
+
} catch {
|
|
170
|
+
log(' No worklist found, skipping stale marking.');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const staleEntries = [];
|
|
175
|
+
let staleCount = 0;
|
|
176
|
+
for (const entry of worklist) {
|
|
177
|
+
if (entry.status === 'enriched') {
|
|
178
|
+
entry.status = 'stale';
|
|
179
|
+
entry.staleReason = 'base-memory-changed';
|
|
180
|
+
entry.staleAt = new Date().toISOString();
|
|
181
|
+
staleCount++;
|
|
182
|
+
staleEntries.push(createSyncEntry({
|
|
183
|
+
action: 'delete',
|
|
184
|
+
keyTag: `key:symbol:${entry.symbolKey.replace(/[^a-zA-Z0-9_-]+/g, '_')}`,
|
|
185
|
+
category: 'architecture',
|
|
186
|
+
tags: ['symbol', entry.language, entry.kind, `project:${projectSlug}`, `file:${entry.filePath}`, `key:symbol:${entry.symbolKey.replace(/[^a-zA-Z0-9_-]+/g, '_')}`],
|
|
187
|
+
source: 'project-context-refresh',
|
|
188
|
+
symbolKey: entry.symbolKey,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (staleCount > 0) {
|
|
194
|
+
await writeFile(worklistPath, `${JSON.stringify(worklist, null, 2)}\n`, 'utf8');
|
|
195
|
+
await appendSyncEntries(dirs.enrichment, staleEntries);
|
|
196
|
+
log(` Marked ${staleCount} enriched symbols as stale in worklist.`);
|
|
197
|
+
log(` Appended ${staleEntries.length} delete entries to sync-manifest.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function runProjectContextCli(projectRoot, options = {}) {
|
|
202
|
+
const refresh = !!options.refresh;
|
|
203
|
+
log(`Target: ${projectRoot} (refresh=${refresh})`);
|
|
204
|
+
|
|
205
|
+
const dirs = await ensureProjectMemoryContextDirs(projectRoot);
|
|
206
|
+
|
|
207
|
+
if (refresh) {
|
|
208
|
+
await runRefresh(projectRoot, dirs);
|
|
209
|
+
} else {
|
|
210
|
+
await runBootstrap(projectRoot, dirs);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function main() {
|
|
215
|
+
const args = process.argv.slice(2);
|
|
216
|
+
const refresh = args.includes('--refresh');
|
|
217
|
+
const projectRoot = resolve(args.filter((a) => a !== '--refresh')[0] ?? process.cwd());
|
|
218
|
+
await runProjectContextCli(projectRoot, { refresh });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
main().catch((error) => {
|
|
222
|
+
console.error('[project-context] FATAL:', error.message);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
});
|