@smartmemory/compose 0.2.53-beta → 0.2.54-beta
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/README.md +28 -0
- package/bin/compose.js +43 -0
- package/dist/assets/{App-Bu9KMtTa.js → App-D_tvTs_n.js} +231 -226
- package/dist/assets/{abnfDiagram-VRR7QNED-6zp3w9rx.js → abnfDiagram-VRR7QNED-Dhb7O5rp.js} +1 -1
- package/dist/assets/{arc-CnLxxuah.js → arc-C4oVdnk2.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-COM46S9k.js → architectureDiagram-ZJ3FMSHR-DqHrb5_F.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-wKzgOwF8.js → blockDiagram-677ZJIJ3-DUyBayYK.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-DIXC_mR6.js → c4Diagram-LMCZKHZV-CxlkWMRK.js} +1 -1
- package/dist/assets/channel-xb4mQfuL.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BqLqZX6m.js → chunk-2Q5K7J3B-C6QJsyOQ.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-CarSmVlX.js → chunk-32BRIVSS-D9NgYu9W.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-D-f_Jn3G.js → chunk-5VM5RSS4-DbD86rhK.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DT42xo8E.js → chunk-EX3LRPZG-E4m4xNMw.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CwY1aZc4.js → chunk-JWPE2WC7-DHGlRWbw.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-DBbZU_KX.js → chunk-MOJQB5TN-BblYnFUq.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-DqxtcLJ3.js → chunk-RYQCIY6F-CGT-88vL.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-Cy4Ixww6.js → chunk-V7JOEXUC-BJJmX0mc.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-CFexfU_a.js → chunk-VR4S4FIN-B4ygduCD.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-uE-zN_vc.js → chunk-XXDRQBXY-7APXuI2s.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BIvgPTgh.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BIvgPTgh.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-DIjpekos.js → cose-bilkent-JH36ORCC-DnVcT6R6.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-Ba2gbQow.js → cynefin-VYW2F7L2-C9qiUSfX.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-njGzb7Tg.js → cynefinDiagram-TSTJHNR4-BowkQhEu.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-DuyZREZ3.js → dagre-VKFMJZFB-Y5V-7U3p.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-Npq-5o3a.js → diagram-FQU43EPY-I7wDE-Xp.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-fm4k3axC.js → diagram-G47NLZAW-DapGa1HI.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-B8EFGrTG.js → diagram-NH7WQ7WH-CHO1JntO.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BC7UHx9Q.js → diagram-OA4YK3LP-mBWP2D1G.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-BqjHLcCX.js → diagram-WEI45ONY-C8KN8YnF.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-D5zXd4Qg.js → ebnfDiagram-CCIWWBDH-ZkJG9NOL.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-DC2FMcra.js → erDiagram-Q63AITRT-DGnL3kW1.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-D7Dx7JU4.js → flowDiagram-23GEKE2U-DHYupfOu.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-8sQH2y5K.js → ganttDiagram-NO4QXBWP-B1B2zpwR.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-Ci_rhxur.js → gitGraphDiagram-IHSO6WYX-B9_OP4Fs.js} +1 -1
- package/dist/assets/index-0Hq9436Q.js +119 -0
- package/dist/assets/index-LIwREYgH.css +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-B9FGQ0Cb.js → infoDiagram-FWYZ7A6U-D-a34TqE.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-BEKLyH6A.js → ishikawaDiagram-FXEZZL3T-h0bUvXGV.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-BM-IDVRo.js → journeyDiagram-5HDEW3XC-B4lm0RBa.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Beb2k3pt.js → kanban-definition-HUTT4EX6-msvfouRX.js} +1 -1
- package/dist/assets/{linear-Dwg7dTpz.js → linear-B0trv2xN.js} +1 -1
- package/dist/assets/{mindmap-definition-LN4V7U3C-CeaynfcE.js → mindmap-definition-LN4V7U3C-CvswbRgd.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-7SVdAVqj.js → pegDiagram-2B236MQR-DZEmIYC7.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-CzqQ4TaE.js → pieDiagram-ENE6RG2P-1ZfqxHkL.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-B9B6W80N.js → quadrantDiagram-ABIIQ3AL-Owx49uiJ.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-BRNLawsr.js → railroadDiagram-RFXS5EU6-B7RiaIGf.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-CmASubeG.js → requirementDiagram-TGXJPOKE-fqmHs_5o.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DiReR0OB.js → sankeyDiagram-HTMAVEWB-D0Q3FeKV.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-DVn5iZKR.js → sequenceDiagram-DBY2YBRQ-CnPuB3fy.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-C8CuDOdp.js → sizeCapture-X5ZJPWSS-CsHoR37V.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-BIdkHglY.js → stateDiagram-2N3HPSRC-GVwpflU5.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DCGIHFu9.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-BlVsYyyN.js → swimlanes-5IMT3BWC-DRR41dsQ.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-C4RhLkZo.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CnTqJmQ2.js → timeline-definition-FHXFAJF6-ePaEemL7.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-D_qZxRhe.js → vennDiagram-L72KCM5P-BAYP8dNF.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-CT-WHFEO.js → wardleyDiagram-EHGQE667-jNVH5Adm.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CVRFHwUQ.js → xychartDiagram-FW5EYKEG-BYDWGXbZ.js} +1 -1
- package/dist/index.html +2 -2
- package/lib/feature-events.js +10 -0
- package/lib/smartmemory-client.js +139 -0
- package/lib/smartmemory-config.js +50 -0
- package/lib/smartmemory-ingest.js +115 -0
- package/lib/smartmemory-sync.js +275 -0
- package/package.json +1 -1
- package/server/gate-log-store.js +11 -1
- package/server/smartmemory-routes.js +113 -0
- package/server/vision-server.js +6 -0
- package/dist/assets/channel-DQJPWkb_.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-CTLbAiUK.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-CTLbAiUK.js +0 -1
- package/dist/assets/index-B7-HQenC.js +0 -119
- package/dist/assets/index-BxRamj_i.css +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-B9yje3Er.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BTLpU45n.js +0 -8
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartmemory-ingest.js — COMP-SMARTMEMORY-INGEST S03
|
|
3
|
+
*
|
|
4
|
+
* Live emitter + shared pure renderers/context builders. Lazy-imported by the
|
|
5
|
+
* two hook sites (lib/feature-events.js, server/gate-log-store.js) only when
|
|
6
|
+
* smartmemory.enabled === true, so the fetch-capable module never loads when
|
|
7
|
+
* the feature is off.
|
|
8
|
+
*
|
|
9
|
+
* Fail-open: emitFeatureEvent/emitGateLogEntry never throw and never affect
|
|
10
|
+
* the caller — the local durable write has already completed by the time
|
|
11
|
+
* this runs. A circuit breaker disables further attempts after 3 consecutive
|
|
12
|
+
* failures so a dead service doesn't spam logs for the rest of the process
|
|
13
|
+
* lifetime.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getSmartmemoryConfig, resolveProjectTag, sourcePathFor } from './smartmemory-config.js';
|
|
17
|
+
import { createSmartmemoryClient } from './smartmemory-client.js';
|
|
18
|
+
|
|
19
|
+
// ── module state ──
|
|
20
|
+
let consecutiveFailures = 0;
|
|
21
|
+
let disabled = false;
|
|
22
|
+
const pending = new Set();
|
|
23
|
+
|
|
24
|
+
/** PURE. One deterministic line per feature-event row. Shared with sync. */
|
|
25
|
+
export function renderFeatureEventContent(row, projectTag) {
|
|
26
|
+
return `[compose:${projectTag}] ${row.ts} ${row.tool} ${row.code ?? row.feature_code ?? '-'} by ${row.actor ?? 'mcp:agent'}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** PURE. One deterministic line per gate-log entry. Shared with sync. */
|
|
30
|
+
export function renderGateLogContent(entry, projectTag) {
|
|
31
|
+
return `[compose:${projectTag}] ${entry.timestamp} gate:${entry.decision} ${entry.feature_code ?? '-'} ${entry.id}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** PURE. context for an event item (cli:compose regime — content-hash dedupe). */
|
|
35
|
+
export function buildFeatureEventContext(projectTag, row) {
|
|
36
|
+
return {
|
|
37
|
+
origin: 'cli:compose',
|
|
38
|
+
project: projectTag,
|
|
39
|
+
source_path: sourcePathFor(projectTag, '.compose/data/feature-events.jsonl'),
|
|
40
|
+
event: row,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildGateLogContext(projectTag, entry) {
|
|
45
|
+
return {
|
|
46
|
+
origin: 'cli:compose',
|
|
47
|
+
project: projectTag,
|
|
48
|
+
source_path: sourcePathFor(projectTag, '.compose/data/gate-log.jsonl'),
|
|
49
|
+
event: entry,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function onFailure(err) {
|
|
54
|
+
consecutiveFailures++;
|
|
55
|
+
if (consecutiveFailures === 1) {
|
|
56
|
+
console.warn('[smartmemory] ingest failed (fail-open): ' + err.message);
|
|
57
|
+
}
|
|
58
|
+
if (consecutiveFailures >= 3) {
|
|
59
|
+
disabled = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function emit(cwd, item, render, buildContext) {
|
|
64
|
+
if (disabled) return;
|
|
65
|
+
let cfg;
|
|
66
|
+
try {
|
|
67
|
+
cfg = getSmartmemoryConfig(cwd);
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (cfg.enabled !== true) return; // no fetch — byte-identity when off
|
|
72
|
+
|
|
73
|
+
let p;
|
|
74
|
+
try {
|
|
75
|
+
const tag = resolveProjectTag(cwd);
|
|
76
|
+
const content = render(item, tag);
|
|
77
|
+
const ctx = buildContext(tag, item);
|
|
78
|
+
const client = createSmartmemoryClient(cfg);
|
|
79
|
+
p = client.ingest(content, ctx);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Synchronous throw (e.g. missing api key) — treat as a failure, never rethrow.
|
|
82
|
+
onFailure(err);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pending.add(p);
|
|
87
|
+
p.then(
|
|
88
|
+
() => { consecutiveFailures = 0; },
|
|
89
|
+
(err) => onFailure(err),
|
|
90
|
+
).finally(() => pending.delete(p));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Fire-and-forget live emit after a durable local append. Never throws. */
|
|
94
|
+
export function emitFeatureEvent(cwd, row) {
|
|
95
|
+
emit(cwd, row, renderFeatureEventContent, buildFeatureEventContext);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function emitGateLogEntry(cwd, entry) {
|
|
99
|
+
emit(cwd, entry, renderGateLogContent, buildGateLogContext);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Await in-flight POSTs (bounded by timeoutMs). Drain point for callers that exit. */
|
|
103
|
+
export async function flushPending(timeoutMs = 3000) {
|
|
104
|
+
if (pending.size === 0) return;
|
|
105
|
+
const settleAll = Promise.allSettled([...pending]);
|
|
106
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
107
|
+
await Promise.race([settleAll, timeout]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Test hook: clear circuit breaker + pending between cases. */
|
|
111
|
+
export function _resetEmitterState() {
|
|
112
|
+
consecutiveFailures = 0;
|
|
113
|
+
disabled = false;
|
|
114
|
+
pending.clear();
|
|
115
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartmemory-sync.js — COMP-SMARTMEMORY-INGEST S05
|
|
3
|
+
*
|
|
4
|
+
* Idempotent backfill/re-sync walker: `compose smartmemory sync`. Walks the
|
|
5
|
+
* four persisted surfaces (feature-events, gate-log, journal entries,
|
|
6
|
+
* per-feature artifacts), in order, and ingests each as one SmartMemory item.
|
|
7
|
+
* Re-running is safe (Decision 4 dedupe regimes): events dedupe by content
|
|
8
|
+
* hash, files dedupe by source_path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { getSmartmemoryConfig, resolveProjectTag, sourcePathFor } from './smartmemory-config.js';
|
|
15
|
+
import { createSmartmemoryClient, SmartmemoryHttpError } from './smartmemory-client.js';
|
|
16
|
+
import {
|
|
17
|
+
renderFeatureEventContent, renderGateLogContent,
|
|
18
|
+
buildFeatureEventContext, buildGateLogContext,
|
|
19
|
+
} from './smartmemory-ingest.js';
|
|
20
|
+
import { readEvents } from './feature-events.js';
|
|
21
|
+
import { readGateLog } from '../server/gate-log-store.js';
|
|
22
|
+
import { parseJournalEntry } from './journal-writer.js';
|
|
23
|
+
import { listFeatures } from './feature-json.js';
|
|
24
|
+
import { loadFeaturesDir, resolveFeaturesPath, resolveJournalPath, relForDisplay } from './project-paths.js';
|
|
25
|
+
|
|
26
|
+
const ARTIFACT_FILES = [
|
|
27
|
+
{ file: 'design.md', kind: 'design' },
|
|
28
|
+
{ file: 'blueprint.md', kind: 'blueprint' },
|
|
29
|
+
{ file: 'plan.md', kind: 'plan' },
|
|
30
|
+
{ file: 'report.md', kind: 'report' },
|
|
31
|
+
{ file: 'audit.json', kind: 'audit' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function gateLogPathFor(cwd) {
|
|
39
|
+
return process.env.COMPOSE_GATE_LOG || join(cwd, '.compose', 'data', 'gate-log.jsonl');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Surface 1: feature-events.jsonl — malformed lines are already skipped by
|
|
44
|
+
* readEvents. May throw (naked fs read) — caller wraps per-surface.
|
|
45
|
+
*/
|
|
46
|
+
function collectFeatureEventItems(cwd, feature, tag) {
|
|
47
|
+
const items = [];
|
|
48
|
+
const events = readEvents(cwd);
|
|
49
|
+
for (const row of events) {
|
|
50
|
+
// OR semantics (blueprint.md ~line 176): a row carrying BOTH fields must
|
|
51
|
+
// still match on `feature_code` even when `code` differs — nullish
|
|
52
|
+
// coalescing (`code ?? feature_code`) would wrongly ignore `feature_code`
|
|
53
|
+
// whenever `code` is present at all.
|
|
54
|
+
if (feature && row.code !== feature && row.feature_code !== feature) continue;
|
|
55
|
+
items.push({ content: renderFeatureEventContent(row, tag), context: buildFeatureEventContext(tag, row) });
|
|
56
|
+
}
|
|
57
|
+
return items;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Surface 2: gate-log.jsonl. May throw (naked fs read) — caller wraps
|
|
62
|
+
* per-surface.
|
|
63
|
+
*/
|
|
64
|
+
function collectGateLogItems(cwd, feature, tag) {
|
|
65
|
+
const items = [];
|
|
66
|
+
const gateEntries = readGateLog({ featureCode: feature ?? undefined, logPath: gateLogPathFor(cwd) });
|
|
67
|
+
for (const entry of gateEntries) {
|
|
68
|
+
items.push({ content: renderGateLogContent(entry, tag), context: buildGateLogContext(tag, entry) });
|
|
69
|
+
}
|
|
70
|
+
return items;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Surface 3: journal entries — file regime (import:compose, source_path =
|
|
75
|
+
* dedupe key). Direct directory walk, NOT getJournalEntries: that reader
|
|
76
|
+
* hard-caps at MAX_LIMIT=500 (wrong for a backfill walker, which must walk
|
|
77
|
+
* everything) and silently drops unreadable/unparseable files instead of
|
|
78
|
+
* accounting for them, which would break this walker's skipped-count
|
|
79
|
+
* contract.
|
|
80
|
+
*
|
|
81
|
+
* A journal entry is defined by filename shape AND parse success (the same
|
|
82
|
+
* contract the canonical reader enforces) — not just "any *.md file". A
|
|
83
|
+
* stray non-entry .md file is simply not a candidate: it is never ingested
|
|
84
|
+
* and never counted as skipped. Per-file read/parse failures increment
|
|
85
|
+
* `skipped` via the returned counter (not thrown) so one broken entry never
|
|
86
|
+
* takes down the rest of the surface; a failure to even list the directory
|
|
87
|
+
* (e.g. it's not a directory at all) DOES throw — the caller wraps that.
|
|
88
|
+
*/
|
|
89
|
+
function collectJournalItems(cwd, feature, tag) {
|
|
90
|
+
const items = [];
|
|
91
|
+
let skipped = 0;
|
|
92
|
+
|
|
93
|
+
const journalDir = resolveJournalPath(cwd);
|
|
94
|
+
if (!existsSync(journalDir)) return { items, skipped };
|
|
95
|
+
|
|
96
|
+
// Exact filename pattern from lib/journal-writer.js:44 (FILENAME_RE, not
|
|
97
|
+
// exported — replicated here rather than editing that module).
|
|
98
|
+
const JOURNAL_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-session-(\d+)-([a-z0-9][a-z0-9-]*[a-z0-9])\.md$/;
|
|
99
|
+
|
|
100
|
+
const candidates = [];
|
|
101
|
+
for (const name of readdirSync(journalDir)) {
|
|
102
|
+
const m = name.match(JOURNAL_FILENAME_RE);
|
|
103
|
+
if (!m) continue; // not a candidate at all — not counted
|
|
104
|
+
candidates.push({ name, date: m[1], session_number: parseInt(m[2], 10) });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Newest-first, mirroring lib/journal-writer.js:920-923 exactly (date desc,
|
|
108
|
+
// then session_number desc) — matters because runSync stops on quota 429,
|
|
109
|
+
// so a partial sync must ingest the newest entries first.
|
|
110
|
+
candidates.sort((a, b) => {
|
|
111
|
+
if (b.date !== a.date) return b.date.localeCompare(a.date);
|
|
112
|
+
return b.session_number - a.session_number;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
for (const { name } of candidates) {
|
|
116
|
+
const absPath = join(journalDir, name);
|
|
117
|
+
let text;
|
|
118
|
+
try {
|
|
119
|
+
text = readFileSync(absPath, 'utf-8');
|
|
120
|
+
} catch {
|
|
121
|
+
skipped++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Always parse — a candidate that fails to parse is skipped, not ingested.
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = parseJournalEntry(text);
|
|
129
|
+
} catch {
|
|
130
|
+
skipped++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (feature && parsed.feature_code !== feature) continue;
|
|
134
|
+
|
|
135
|
+
items.push({
|
|
136
|
+
content: text,
|
|
137
|
+
context: {
|
|
138
|
+
origin: 'import:compose',
|
|
139
|
+
project: tag,
|
|
140
|
+
source_path: sourcePathFor(tag, relForDisplay(cwd, absPath)),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { items, skipped };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Surface 4: per-feature artifacts — file regime. Per-file read failures
|
|
150
|
+
* increment `skipped` (not thrown); a failure to list features or the
|
|
151
|
+
* features directory itself DOES throw — the caller wraps that.
|
|
152
|
+
*/
|
|
153
|
+
function collectArtifactItems(cwd, feature, tag) {
|
|
154
|
+
const items = [];
|
|
155
|
+
let skipped = 0;
|
|
156
|
+
|
|
157
|
+
const allFeatures = listFeatures(cwd, loadFeaturesDir(cwd));
|
|
158
|
+
const targetFeatures = feature ? allFeatures.filter((f) => f.code === feature) : allFeatures;
|
|
159
|
+
const featuresAbsDir = resolveFeaturesPath(cwd);
|
|
160
|
+
for (const f of targetFeatures) {
|
|
161
|
+
const folder = join(featuresAbsDir, f.code);
|
|
162
|
+
for (const { file, kind } of ARTIFACT_FILES) {
|
|
163
|
+
const absPath = join(folder, file);
|
|
164
|
+
if (!existsSync(absPath)) continue;
|
|
165
|
+
let text;
|
|
166
|
+
try {
|
|
167
|
+
text = readFileSync(absPath, 'utf-8');
|
|
168
|
+
} catch {
|
|
169
|
+
skipped++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
items.push({
|
|
173
|
+
content: text,
|
|
174
|
+
context: {
|
|
175
|
+
origin: 'import:compose',
|
|
176
|
+
project: tag,
|
|
177
|
+
source_path: sourcePathFor(tag, relForDisplay(cwd, absPath)),
|
|
178
|
+
artifact: { feature_code: f.code, kind },
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { items, skipped };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Walk the four surfaces and build the flat, ordered list of {content,
|
|
189
|
+
* context} items to ingest, honoring the `--feature CODE` filter (Decision 5
|
|
190
|
+
* semantics): entries with no feature association are excluded when `feature`
|
|
191
|
+
* is set, included in a full sync.
|
|
192
|
+
*
|
|
193
|
+
* Each surface is collected independently, wrapped in its own try/catch: a
|
|
194
|
+
* surface whose top-level read throws (e.g. a corrupted directory in its
|
|
195
|
+
* path) is reported (one `skipped` increment + a console.warn naming the
|
|
196
|
+
* surface and the error) and does NOT abort the other three surfaces.
|
|
197
|
+
*/
|
|
198
|
+
function collectItems(cwd, feature, tag) {
|
|
199
|
+
const items = [];
|
|
200
|
+
let skipped = 0;
|
|
201
|
+
|
|
202
|
+
const surfaces = [
|
|
203
|
+
{ name: 'feature-events', collect: () => ({ items: collectFeatureEventItems(cwd, feature, tag), skipped: 0 }) },
|
|
204
|
+
{ name: 'gate-log', collect: () => ({ items: collectGateLogItems(cwd, feature, tag), skipped: 0 }) },
|
|
205
|
+
{ name: 'journal', collect: () => collectJournalItems(cwd, feature, tag) },
|
|
206
|
+
{ name: 'artifacts', collect: () => collectArtifactItems(cwd, feature, tag) },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
for (const surface of surfaces) {
|
|
210
|
+
try {
|
|
211
|
+
const result = surface.collect();
|
|
212
|
+
items.push(...result.items);
|
|
213
|
+
skipped += result.skipped;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
skipped++;
|
|
216
|
+
console.warn(`[smartmemory-sync] ${surface.name} surface read failed (skipping this surface): ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { items, skipped };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Walk the four persisted surfaces and ingest each item (idempotent).
|
|
225
|
+
* @param {{ cwd: string, dryRun?: boolean, feature?: string|null }} opts
|
|
226
|
+
* @returns {Promise<{ ingested:number, unchanged:number, skipped:number, failed:number, stoppedOnQuota:boolean }>}
|
|
227
|
+
*/
|
|
228
|
+
export async function runSync({ cwd, dryRun = false, feature = null }) {
|
|
229
|
+
const cfg = getSmartmemoryConfig(cwd);
|
|
230
|
+
if (cfg.enabled !== true && !dryRun) {
|
|
231
|
+
throw new Error('smartmemory not enabled');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const tag = resolveProjectTag(cwd);
|
|
235
|
+
const { items, skipped } = collectItems(cwd, feature, tag);
|
|
236
|
+
|
|
237
|
+
let ingested = 0;
|
|
238
|
+
let unchanged = 0;
|
|
239
|
+
let failed = 0;
|
|
240
|
+
let stoppedOnQuota = false;
|
|
241
|
+
|
|
242
|
+
if (dryRun) {
|
|
243
|
+
return { ingested: items.length, unchanged: 0, skipped, failed: 0, stoppedOnQuota: false };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const client = createSmartmemoryClient(cfg);
|
|
247
|
+
|
|
248
|
+
for (const item of items) {
|
|
249
|
+
let result;
|
|
250
|
+
try {
|
|
251
|
+
result = await client.ingest(item.content, item.context);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof SmartmemoryHttpError && err.status === 429) {
|
|
254
|
+
await sleep(1000);
|
|
255
|
+
try {
|
|
256
|
+
result = await client.ingest(item.content, item.context);
|
|
257
|
+
} catch (err2) {
|
|
258
|
+
if (err2 instanceof SmartmemoryHttpError && err2.status === 429) {
|
|
259
|
+
stoppedOnQuota = true;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
failed++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
failed++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (result.unchanged) unchanged++;
|
|
271
|
+
else ingested++;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { ingested, unchanged, skipped, failed, stoppedOnQuota };
|
|
275
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.54-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|
package/server/gate-log-store.js
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import { appendFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
-
import { getDataDir } from './project-root.js';
|
|
24
|
+
import { getDataDir, getTargetRoot } from './project-root.js';
|
|
25
|
+
import { getSmartmemoryConfig } from '../lib/smartmemory-config.js';
|
|
25
26
|
|
|
26
27
|
// Gate log is project-scoped (mirrors sessions.json, active-build.json etc).
|
|
27
28
|
// COMPOSE_GATE_LOG env var overrides the path — read dynamically so tests can inject it.
|
|
@@ -65,6 +66,15 @@ export function appendGateLogEntry(entry) {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf8');
|
|
69
|
+
// COMP-SMARTMEMORY-INGEST: fail-open live emit after the durable append.
|
|
70
|
+
try {
|
|
71
|
+
const cwd = getTargetRoot();
|
|
72
|
+
if (getSmartmemoryConfig(cwd).enabled === true) {
|
|
73
|
+
import('../lib/smartmemory-ingest.js')
|
|
74
|
+
.then((m) => m.emitGateLogEntry(cwd, entry))
|
|
75
|
+
.catch(() => {});
|
|
76
|
+
}
|
|
77
|
+
} catch { /* fail-open */ }
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
/**
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartmemory-routes.js — COMP-SMARTMEMORY-RECALL S01.
|
|
3
|
+
*
|
|
4
|
+
* Route:
|
|
5
|
+
* GET /api/smartmemory/recall?featureCode=<CODE> — ranked prior-context
|
|
6
|
+
* lookup for the cockpit's Recall tab, backed by INGEST's SmartMemory
|
|
7
|
+
* client + config (lib/smartmemory-client.js, lib/smartmemory-config.js).
|
|
8
|
+
*
|
|
9
|
+
* Degrade-never-fail (the qa-scope-routes.js model): every branch returns a
|
|
10
|
+
* shaped 200, never a throw. Flag OFF (or no workspace root) answers from
|
|
11
|
+
* config alone — zero client construction, zero SmartMemory-bound traffic.
|
|
12
|
+
*
|
|
13
|
+
* Not on the auth allowlist (server/index.js) — auth-gated in remote mode
|
|
14
|
+
* like the vision routes, since results can carry local project context.
|
|
15
|
+
*/
|
|
16
|
+
import { getSmartmemoryConfig, resolveProjectTag as defaultResolveProjectTag } from '../lib/smartmemory-config.js';
|
|
17
|
+
import { createSmartmemoryClient } from '../lib/smartmemory-client.js';
|
|
18
|
+
import { readFeature as defaultReadFeature } from '../lib/feature-json.js';
|
|
19
|
+
import { resolveFeaturesPath as defaultResolveFeaturesPath } from '../lib/project-paths.js';
|
|
20
|
+
|
|
21
|
+
function defaultCreateClient(cfg) {
|
|
22
|
+
return createSmartmemoryClient(cfg);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shortReason(e) {
|
|
26
|
+
return e?.message?.slice(0, 200) || 'unreachable';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeProjectTag(resolveProjectTag, root) {
|
|
30
|
+
try {
|
|
31
|
+
return resolveProjectTag(root);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Defensive adapter over the SmartMemory hit shape (multiple field-name fallbacks). */
|
|
38
|
+
function mapHit(hit) {
|
|
39
|
+
const content = hit?.content ?? hit?.text ?? '';
|
|
40
|
+
return {
|
|
41
|
+
id: hit?.id ?? hit?.item_id ?? null,
|
|
42
|
+
snippet: content.length > 280 ? content.slice(0, 280) + '…' : content,
|
|
43
|
+
score: hit?.score ?? null,
|
|
44
|
+
memoryType: hit?.memory_type ?? hit?.memoryType ?? null,
|
|
45
|
+
ts: hit?.context?.event?.ts ?? hit?.context?.ts ?? hit?.ts ?? null,
|
|
46
|
+
project: hit?.context?.project ?? null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {import('express').Express} app
|
|
52
|
+
* @param {object} [deps]
|
|
53
|
+
* @param {Function} [deps.getConfig] — (cwd) => {} | {enabled,baseUrl,apiKeyEnv,timeoutMs}
|
|
54
|
+
* @param {Function} [deps.createClient] — (cfg) => { search(query, opts) }
|
|
55
|
+
* @param {Function} [deps.readFeature] — (cwd, code, featuresDir) => FeatureJson|null
|
|
56
|
+
* @param {Function} [deps.resolveFeaturesPath] — (cwd) => absolute features dir
|
|
57
|
+
* @param {Function} [deps.resolveProjectTag] — (cwd) => canonical project tag
|
|
58
|
+
*/
|
|
59
|
+
export function attachSmartmemoryRoutes(app, {
|
|
60
|
+
getConfig = getSmartmemoryConfig,
|
|
61
|
+
createClient = defaultCreateClient,
|
|
62
|
+
readFeature = defaultReadFeature,
|
|
63
|
+
resolveFeaturesPath = defaultResolveFeaturesPath,
|
|
64
|
+
resolveProjectTag = defaultResolveProjectTag,
|
|
65
|
+
} = {}) {
|
|
66
|
+
app.get('/api/smartmemory/recall', async (req, res) => {
|
|
67
|
+
const root = req.workspace?.root;
|
|
68
|
+
if (!root) {
|
|
69
|
+
return res.json({ enabled: false });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let cfg;
|
|
73
|
+
try {
|
|
74
|
+
cfg = getConfig(root);
|
|
75
|
+
} catch {
|
|
76
|
+
cfg = {};
|
|
77
|
+
}
|
|
78
|
+
if (cfg?.enabled !== true) {
|
|
79
|
+
return res.json({ enabled: false });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const featureCode = (req.query.featureCode || '').toString().trim();
|
|
83
|
+
if (!featureCode) {
|
|
84
|
+
return res.json({ enabled: true, available: true, results: [], invalidFeatureCode: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let feature;
|
|
88
|
+
try {
|
|
89
|
+
feature = readFeature(root, featureCode, resolveFeaturesPath(root));
|
|
90
|
+
} catch {
|
|
91
|
+
feature = null;
|
|
92
|
+
}
|
|
93
|
+
const desc = feature?.description || '';
|
|
94
|
+
const query = desc ? `${featureCode} ${desc}` : featureCode;
|
|
95
|
+
|
|
96
|
+
let hits;
|
|
97
|
+
try {
|
|
98
|
+
const client = createClient(cfg);
|
|
99
|
+
hits = await client.search(query, { top_k: 10 });
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return res.json({ enabled: true, available: false, error: shortReason(e) });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const results = (Array.isArray(hits) ? hits : (hits?.results ?? [])).map(mapHit);
|
|
105
|
+
return res.json({
|
|
106
|
+
enabled: true,
|
|
107
|
+
available: true,
|
|
108
|
+
featureCode,
|
|
109
|
+
project: safeProjectTag(resolveProjectTag, root),
|
|
110
|
+
results,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
package/server/vision-server.js
CHANGED
|
@@ -30,6 +30,7 @@ import { attachSettingsRoutes } from './settings-routes.js';
|
|
|
30
30
|
import { attachHealthRoutes } from './health-routes.js';
|
|
31
31
|
import { attachValidateRoutes } from './validate-routes.js';
|
|
32
32
|
import { attachQaScopeRoutes } from './qa-scope-routes.js';
|
|
33
|
+
import { attachSmartmemoryRoutes } from './smartmemory-routes.js';
|
|
33
34
|
import { attachDesignRoutes } from './design-routes.js';
|
|
34
35
|
import { DesignSessionManager } from './design-session.js';
|
|
35
36
|
import { attachPipelineRoutes } from './pipeline-routes.js';
|
|
@@ -102,6 +103,11 @@ export class VisionServer {
|
|
|
102
103
|
// Read-only GET /api/qa-scope?featureCode=…; reads off req.workspace.
|
|
103
104
|
attachQaScopeRoutes(app);
|
|
104
105
|
|
|
106
|
+
// ── SmartMemory recall (COMP-SMARTMEMORY-RECALL, opt-in) ───────────────
|
|
107
|
+
// Read-only GET /api/smartmemory/recall?featureCode=…; degrade-never-fail,
|
|
108
|
+
// answers {enabled:false} from config alone when the flag is OFF.
|
|
109
|
+
attachSmartmemoryRoutes(app);
|
|
110
|
+
|
|
105
111
|
// ── Vision CRUD + lifecycle routes ─────────────────────────────────────
|
|
106
112
|
attachVisionRoutes(app, {
|
|
107
113
|
store: this.store,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{ag as o,ah as n}from"./App-Bu9KMtTa.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-Cy4Ixww6.js";import{_ as i}from"./App-Bu9KMtTa.js";import"./chunk-5VM5RSS4-D-f_Jn3G.js";import"./chunk-XXDRQBXY-uE-zN_vc.js";import"./chunk-VR4S4FIN-CFexfU_a.js";import"./chunk-32BRIVSS-CarSmVlX.js";import"./mobile-CNLMhdFP.js";import"./index-B7-HQenC.js";import"./graph-xkel59g2.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-Cy4Ixww6.js";import{_ as i}from"./App-Bu9KMtTa.js";import"./chunk-5VM5RSS4-D-f_Jn3G.js";import"./chunk-XXDRQBXY-uE-zN_vc.js";import"./chunk-VR4S4FIN-CFexfU_a.js";import"./chunk-32BRIVSS-CarSmVlX.js";import"./mobile-CNLMhdFP.js";import"./index-B7-HQenC.js";import"./graph-xkel59g2.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|