hippo-memory 1.61.0 → 1.63.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/README.md +37 -53
- package/dist/agent-memories/apply.d.ts +3 -1
- package/dist/agent-memories/apply.js +23 -1
- package/dist/agent-memories/claude-code.d.ts +4 -1
- package/dist/agent-memories/claude-code.js +53 -9
- package/dist/agent-memories/report.d.ts +1 -0
- package/dist/agent-memories/report.js +2 -2
- package/dist/agent-memories/sync.d.ts +3 -3
- package/dist/agent-memories/sync.js +46 -24
- package/dist/agent-memories/types.d.ts +0 -2
- package/dist/ambient-store.d.ts +4 -4
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +7 -10
- package/dist/api/assemble.js +62 -66
- package/dist/api/audit.d.ts +2 -2
- package/dist/api/audit.js +2 -2
- package/dist/api/auth.d.ts +6 -6
- package/dist/api/auth.js +5 -7
- package/dist/api/context-select.d.ts +50 -0
- package/dist/api/context-select.js +342 -0
- package/dist/api/context-types.d.ts +12 -17
- package/dist/api/context.d.ts +5 -4
- package/dist/api/context.js +208 -535
- package/dist/api/dormant.js +1 -1
- package/dist/api/drill-down.d.ts +4 -4
- package/dist/api/drill-down.js +56 -41
- package/dist/api/outcome.d.ts +8 -13
- package/dist/api/outcome.js +13 -17
- package/dist/api/promote.d.ts +5 -8
- package/dist/api/promote.js +55 -66
- package/dist/api/quarantine.js +3 -2
- package/dist/api/recall-types.d.ts +43 -55
- package/dist/api/recall.d.ts +3 -3
- package/dist/api/recall.js +310 -456
- package/dist/api/remember.d.ts +2 -2
- package/dist/api/sleep.d.ts +7 -35
- package/dist/api/sleep.js +206 -220
- package/dist/api/tokens.d.ts +2 -2
- package/dist/api/tokens.js +2 -2
- package/dist/api/types.d.ts +6 -10
- package/dist/api/types.js +2 -4
- package/dist/audit-prune.d.ts +4 -6
- package/dist/audit-prune.js +3 -5
- package/dist/audit.js +11 -33
- package/dist/auth.d.ts +8 -9
- package/dist/auth.js +5 -7
- package/dist/autolearn.d.ts +1 -1
- package/dist/autolearn.js +1 -1
- package/dist/availability.js +3 -5
- package/dist/capture/command.d.ts +5 -4
- package/dist/capture/command.js +8 -22
- package/dist/capture/compact.d.ts +2 -2
- package/dist/capture/compact.js +11 -15
- package/dist/capture/extract.js +37 -115
- package/dist/capture-error.d.ts +1 -1
- package/dist/capture-error.js +1 -1
- package/dist/churn-git.d.ts +1 -1
- package/dist/churn-git.js +1 -1
- package/dist/cli/audit.js +3 -4
- package/dist/cli/auth.js +3 -6
- package/dist/cli/briefs.js +324 -306
- package/dist/cli/context.js +44 -34
- package/dist/cli/continuity.js +283 -271
- package/dist/cli/curate.d.ts +1 -1
- package/dist/cli/curate.js +43 -58
- package/dist/cli/dag.js +5 -9
- package/dist/cli/decisions.js +334 -345
- package/dist/cli/explain.js +68 -61
- package/dist/cli/goals.js +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/maintenance.js +62 -51
- package/dist/cli/playbooks.js +391 -379
- package/dist/cli/projects.js +11 -6
- package/dist/cli/recall.js +30 -44
- package/dist/cli/remember.js +118 -87
- package/dist/cli/session-hooks.js +106 -115
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.js +267 -250
- package/dist/cli/shared.js +3 -3
- package/dist/cli/slack.js +1 -1
- package/dist/cli/sleep.js +27 -1
- package/dist/cli/status.d.ts +4 -4
- package/dist/cli/status.js +80 -76
- package/dist/cli/transfer.js +88 -107
- package/dist/cli/usage.js +9 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -9
- package/dist/compaction-record.d.ts +2 -2
- package/dist/compaction-record.js +89 -68
- package/dist/compare.d.ts +11 -16
- package/dist/compare.js +11 -16
- package/dist/config.d.ts +19 -18
- package/dist/config.js +90 -67
- package/dist/connectors/github/backfill.d.ts +2 -2
- package/dist/connectors/github/backfill.js +8 -15
- package/dist/connectors/github/cli-impl.js +3 -8
- package/dist/connectors/github/deletion.d.ts +5 -12
- package/dist/connectors/github/deletion.js +5 -12
- package/dist/connectors/github/dlq.d.ts +6 -9
- package/dist/connectors/github/dlq.js +2 -3
- package/dist/connectors/github/ingest.d.ts +5 -7
- package/dist/connectors/github/ingest.js +8 -12
- package/dist/connectors/github/octokit-client.d.ts +3 -5
- package/dist/connectors/github/octokit-client.js +5 -6
- package/dist/connectors/github/signature.d.ts +9 -39
- package/dist/connectors/github/signature.js +9 -39
- package/dist/connectors/github/tenant-routing.d.ts +1 -1
- package/dist/connectors/github/tenant-routing.js +1 -1
- package/dist/connectors/github/transform.js +2 -2
- package/dist/connectors/github/types.d.ts +2 -10
- package/dist/connectors/github/types.js +1 -3
- package/dist/connectors/slack/deletion.d.ts +3 -8
- package/dist/connectors/slack/deletion.js +3 -8
- package/dist/connectors/slack/dlq.d.ts +1 -1
- package/dist/connectors/slack/ingest.d.ts +1 -1
- package/dist/connectors/slack/ingest.js +7 -16
- package/dist/connectors/slack/signature.d.ts +1 -1
- package/dist/connectors/slack/tenant-routing.d.ts +3 -5
- package/dist/connectors/slack/tenant-routing.js +3 -5
- package/dist/connectors/slack/transform.d.ts +5 -6
- package/dist/connectors/slack/transform.js +5 -6
- package/dist/connectors/slack/types.d.ts +2 -6
- package/dist/connectors/slack/types.js +1 -3
- package/dist/connectors/slack/web-client.js +10 -3
- package/dist/connectors/slack/workspaces.d.ts +3 -5
- package/dist/connectors/slack/workspaces.js +3 -5
- package/dist/consolidate/conflicts.js +3 -14
- package/dist/consolidate/decay.js +9 -29
- package/dist/consolidate/llm-passes.js +4 -5
- package/dist/consolidate/merge.js +8 -23
- package/dist/consolidate/run.d.ts +1 -8
- package/dist/consolidate/run.js +3 -25
- package/dist/consolidate/sleep.js +5 -17
- package/dist/consolidate/traces.js +9 -21
- package/dist/customer-notes.d.ts +5 -7
- package/dist/customer-notes.js +82 -76
- package/dist/dag.d.ts +10 -21
- package/dist/dag.js +189 -203
- package/dist/db/continuity.js +2 -2
- package/dist/db/migrations/v14.js +1 -1
- package/dist/db/migrations/v15.js +1 -2
- package/dist/db/migrations/v16.js +3 -4
- package/dist/db/migrations/v17.js +2 -3
- package/dist/db/migrations/v19.js +1 -1
- package/dist/db/migrations/v20.js +1 -1
- package/dist/db/migrations/v21.js +2 -6
- package/dist/db/migrations/v22.js +2 -4
- package/dist/db/migrations/v23.js +1 -1
- package/dist/db/migrations/v24.js +4 -6
- package/dist/db/migrations/v25.js +2 -3
- package/dist/db/migrations/v26.js +3 -3
- package/dist/db/migrations/v27.js +2 -10
- package/dist/db/migrations/v28.js +5 -8
- package/dist/db/migrations/v29.js +3 -4
- package/dist/db/migrations/v30.js +2 -2
- package/dist/db/migrations/v31.js +1 -1
- package/dist/db/migrations/v32.js +1 -1
- package/dist/db/migrations/v33.js +3 -3
- package/dist/db/migrations/v34.js +1 -1
- package/dist/db/migrations/v35.js +3 -4
- package/dist/db/migrations/v36.js +3 -4
- package/dist/db/migrations/v37.js +5 -5
- package/dist/db/migrations/v38.js +7 -8
- package/dist/db/migrations/v39.js +1 -1
- package/dist/db/migrations/v40.js +4 -16
- package/dist/db/migrations/v41.js +3 -4
- package/dist/db/migrations/v42.js +3 -4
- package/dist/db/migrations/v45.js +1 -1
- package/dist/db/migrations/v46.js +1 -1
- package/dist/db/migrations/v47.js +1 -1
- package/dist/db/migrations/v48.js +1 -1
- package/dist/decisions.d.ts +2 -2
- package/dist/decisions.js +97 -80
- package/dist/dedupe.js +86 -61
- package/dist/delivery-recorder.js +154 -135
- package/dist/doctor.js +129 -110
- package/dist/dormant.js +1 -4
- package/dist/embedding-provider.d.ts +4 -8
- package/dist/embedding-provider.js +4 -8
- package/dist/embeddings.js +55 -47
- package/dist/env.d.ts +1 -1
- package/dist/env.js +12 -12
- package/dist/escape.d.ts +5 -0
- package/dist/escape.js +10 -0
- package/dist/eval-stats.d.ts +1 -2
- package/dist/eval-stats.js +1 -2
- package/dist/eval-suite.js +27 -21
- package/dist/extract.js +4 -9
- package/dist/failure-log.d.ts +3 -3
- package/dist/failure-log.js +1 -1
- package/dist/forward-claim-detector.d.ts +2 -4
- package/dist/forward-claim-detector.js +6 -11
- package/dist/goals.d.ts +3 -3
- package/dist/goals.js +103 -91
- package/dist/graph/read.d.ts +2 -2
- package/dist/graph/read.js +5 -6
- package/dist/graph/types.d.ts +8 -8
- package/dist/graph/write.d.ts +7 -14
- package/dist/graph/write.js +16 -23
- package/dist/graph-extract.d.ts +7 -8
- package/dist/graph-extract.js +62 -72
- package/dist/graph-recall.d.ts +2 -2
- package/dist/graph-recall.js +55 -49
- package/dist/graph-stream.d.ts +5 -6
- package/dist/graph-stream.js +66 -57
- package/dist/graph-view.d.ts +2 -2
- package/dist/graph-view.js +7 -7
- package/dist/half-life-migration.d.ts +1 -2
- package/dist/half-life-migration.js +2 -3
- package/dist/hooks/codex-session.js +1 -1
- package/dist/hooks/codex-wrapper.d.ts +1 -1
- package/dist/hooks/codex-wrapper.js +3 -2
- package/dist/hooks/json-hooks.d.ts +2 -2
- package/dist/hooks/json-hooks.js +5 -4
- package/dist/hooks/opencode.d.ts +1 -1
- package/dist/hooks/opencode.js +5 -4
- package/dist/hooks/shared.d.ts +3 -7
- package/dist/hooks/shared.js +1 -8
- package/dist/http-util.d.ts +2 -3
- package/dist/http-util.js +3 -0
- package/dist/importers/core.d.ts +2 -9
- package/dist/importers/core.js +15 -30
- package/dist/importers/sources.js +2 -1
- package/dist/importers/vault.js +2 -20
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +46 -39
- package/dist/instruction-detect.d.ts +1 -1
- package/dist/instruction-detect.js +1 -1
- package/dist/invalidation.d.ts +3 -0
- package/dist/invalidation.js +160 -114
- package/dist/json.d.ts +5 -0
- package/dist/json.js +4 -0
- package/dist/judgment.js +1 -2
- package/dist/local-embedding.js +1 -1
- package/dist/mcp/admin-tools.js +7 -17
- package/dist/mcp/format.js +1 -1
- package/dist/mcp/framing.js +3 -6
- package/dist/mcp/protocol.d.ts +2 -5
- package/dist/mcp/protocol.js +1 -3
- package/dist/mcp/recall-tools.js +12 -15
- package/dist/mcp/request.js +4 -3
- package/dist/mcp/session-state.js +2 -3
- package/dist/mcp/stdio.js +2 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/memory-value-weights.d.ts +5 -8
- package/dist/memory-value-weights.js +5 -8
- package/dist/memory-value.d.ts +13 -13
- package/dist/memory-value.js +26 -37
- package/dist/memory.d.ts +20 -22
- package/dist/memory.js +24 -48
- package/dist/multihop.d.ts +1 -1
- package/dist/multihop.js +3 -2
- package/dist/owner-validation.d.ts +4 -5
- package/dist/owner-validation.js +4 -5
- package/dist/physics.d.ts +4 -4
- package/dist/physics.js +7 -9
- package/dist/policies.d.ts +9 -10
- package/dist/policies.js +96 -81
- package/dist/postinstall.js +3 -6
- package/dist/predictions/planning-fallacy.d.ts +9 -14
- package/dist/predictions/planning-fallacy.js +10 -16
- package/dist/predictions/store.d.ts +15 -23
- package/dist/predictions/store.js +36 -33
- package/dist/processes.d.ts +2 -7
- package/dist/processes.js +88 -72
- package/dist/project-briefs.d.ts +2 -3
- package/dist/project-briefs.js +141 -118
- package/dist/project-identity.d.ts +22 -9
- package/dist/project-identity.js +47 -12
- package/dist/project-merge.d.ts +28 -5
- package/dist/project-merge.js +213 -46
- package/dist/project-remote.d.ts +12 -0
- package/dist/project-remote.js +138 -0
- package/dist/prompt-recall.js +1 -2
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.js +1 -1
- package/dist/raw-archive.d.ts +9 -0
- package/dist/raw-archive.js +70 -53
- package/dist/recall-history.d.ts +19 -20
- package/dist/recall-history.js +24 -42
- package/dist/recall-pipeline.js +4 -28
- package/dist/recall-scope.d.ts +7 -8
- package/dist/recall-scope.js +7 -8
- package/dist/recall-trace.d.ts +5 -9
- package/dist/recall-trace.js +6 -10
- package/dist/refine-llm.d.ts +1 -1
- package/dist/refine-llm.js +2 -2
- package/dist/reject-flow.d.ts +3 -4
- package/dist/reject-flow.js +122 -117
- package/dist/rejection.d.ts +5 -6
- package/dist/rejection.js +7 -15
- package/dist/rerankers/clef.d.ts +1 -1
- package/dist/rerankers/jev.d.ts +1 -2
- package/dist/rerankers/jev.js +4 -5
- package/dist/rerankers/llm.d.ts +1 -2
- package/dist/rerankers/llm.js +1 -2
- package/dist/rerankers/types.d.ts +1 -2
- package/dist/rrf.d.ts +2 -2
- package/dist/rrf.js +2 -2
- package/dist/search/bm25-search.d.ts +1 -1
- package/dist/search/bm25-search.js +2 -1
- package/dist/search/boosts.js +2 -1
- package/dist/search/hybrid.d.ts +1 -1
- package/dist/search/hybrid.js +2 -1
- package/dist/search/physics-search.d.ts +1 -1
- package/dist/search/physics-search.js +2 -1
- package/dist/search/types.d.ts +2 -0
- package/dist/search/types.js +3 -1
- package/dist/secret-detect.d.ts +4 -5
- package/dist/secret-detect.js +6 -10
- package/dist/server/auth.js +5 -5
- package/dist/server/client-ip.js +1 -1
- package/dist/server/cursor.js +2 -1
- package/dist/server/mcp-http.js +4 -4
- package/dist/server/request.d.ts +3 -6
- package/dist/server/request.js +6 -7
- package/dist/server/routes/admin.js +5 -4
- package/dist/server/routes/customer-notes.js +6 -5
- package/dist/server/routes/decisions.js +4 -3
- package/dist/server/routes/incidents.js +7 -5
- package/dist/server/routes/memories.js +7 -7
- package/dist/server/routes/policies.js +3 -2
- package/dist/server/routes/predictions.js +12 -15
- package/dist/server/routes/processes.js +3 -2
- package/dist/server/routes/project-briefs.js +8 -7
- package/dist/server/routes/recall.js +95 -93
- package/dist/server/routes/skills.js +6 -5
- package/dist/server/types.d.ts +1 -1
- package/dist/server/validation.d.ts +1 -2
- package/dist/server/validation.js +7 -14
- package/dist/server-detect.js +72 -58
- package/dist/server.d.ts +2 -2
- package/dist/server.js +131 -117
- package/dist/shared.d.ts +26 -17
- package/dist/shared.js +102 -104
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +88 -72
- package/dist/store/audit-event.d.ts +2 -2
- package/dist/store/audit-event.js +1 -1
- package/dist/store/candidates.d.ts +2 -2
- package/dist/store/candidates.js +4 -3
- package/dist/store/conflicts.js +30 -22
- package/dist/store/delete-and-batch.d.ts +11 -14
- package/dist/store/delete-and-batch.js +40 -91
- package/dist/store/entry-reads.d.ts +17 -25
- package/dist/store/entry-reads.js +59 -40
- package/dist/store/entry-row.d.ts +6 -24
- package/dist/store/entry-row.js +6 -24
- package/dist/store/entry-writes.d.ts +6 -7
- package/dist/store/entry-writes.js +13 -11
- package/dist/store/handoffs.d.ts +1 -1
- package/dist/store/handoffs.js +7 -10
- package/dist/store/index-and-stats.d.ts +2 -6
- package/dist/store/index-and-stats.js +4 -10
- package/dist/store/mirrors.d.ts +6 -19
- package/dist/store/mirrors.js +14 -39
- package/dist/store/open.js +9 -31
- package/dist/store/rows.d.ts +5 -11
- package/dist/store/rows.js +6 -11
- package/dist/store/search-rows.d.ts +17 -34
- package/dist/store/search-rows.js +34 -56
- package/dist/store/sessions.d.ts +4 -5
- package/dist/store/sessions.js +5 -6
- package/dist/store/summaries.d.ts +13 -17
- package/dist/store/summaries.js +26 -70
- package/dist/support-bundle.js +4 -8
- package/dist/tenant.d.ts +1 -5
- package/dist/token-ledger.d.ts +1 -1
- package/dist/token-ledger.js +3 -5
- package/dist/trace.js +1 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/project-briefs.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (docs/plans/2026-05-30-e2-project-brief-object.md).
|
|
2
|
+
* Project_brief first-class object.
|
|
4
3
|
*
|
|
5
4
|
* A `project_brief` is the living, repo-scoped summary of a repository's state: a
|
|
6
5
|
* `summary` body scoped to a `repo`, evolving via the supersede delta lifecycle.
|
|
@@ -31,6 +30,7 @@ import { createMemory, Layer } from './memory.js';
|
|
|
31
30
|
import { appendAuditEvent } from './audit.js';
|
|
32
31
|
import { objectHalfLifeDays } from './half-life-migration.js';
|
|
33
32
|
import { keysetAfter } from './keyset.js';
|
|
33
|
+
import { escapeLike } from './escape.js';
|
|
34
34
|
export const VALID_BRIEF_STATES = new Set([
|
|
35
35
|
'active',
|
|
36
36
|
'superseded',
|
|
@@ -103,9 +103,83 @@ const BRIEF_COLS = `
|
|
|
103
103
|
function buildBriefContent(repo, summary) {
|
|
104
104
|
return `${repo}\n\n${summary}`;
|
|
105
105
|
}
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
106
|
+
// Preflight the supersede target BEFORE inserting the new row (so the new
|
|
107
|
+
// autoincrement id can never be its own supersede target); read the
|
|
108
|
+
// predecessor version in the same SELECT for server-derived versioning.
|
|
109
|
+
// Mirrors saveSkill / saveProcess.
|
|
110
|
+
function preflightBriefSupersede(db, tenantId, supersedesId) {
|
|
111
|
+
// SAFETY: SELECT projects exactly status, version; .get() returns that
|
|
112
|
+
// shape for the matching row, or undefined when no brief/tenant pair matches.
|
|
113
|
+
const pred = db.prepare(`SELECT status, version FROM project_briefs WHERE id = ? AND tenant_id = ?`).get(supersedesId, tenantId);
|
|
114
|
+
if (!pred) {
|
|
115
|
+
throw new NotFoundError(`saveProjectBrief: brief ${supersedesId} to supersede not found for tenant ${tenantId}`);
|
|
116
|
+
}
|
|
117
|
+
if (pred.status !== 'active') {
|
|
118
|
+
throw new ConflictError(`saveProjectBrief: brief ${supersedesId} is not active (status='${pred.status}'); only active briefs can be superseded.`);
|
|
119
|
+
}
|
|
120
|
+
return pred.version + 1;
|
|
121
|
+
}
|
|
122
|
+
function insertBriefRow(db, memoryId, w, version) {
|
|
123
|
+
const result = db.prepare(`
|
|
124
|
+
INSERT INTO project_briefs(
|
|
125
|
+
memory_id, tenant_id, repo, summary, version,
|
|
126
|
+
status, superseded_by, superseded_at, change_summary, closed_at, created_at
|
|
127
|
+
) VALUES (?, ?, ?, ?, ?, 'active', NULL, NULL, ?, NULL, ?)
|
|
128
|
+
`).run(memoryId, w.tenantId, w.repo, w.summary, version, w.changeSummary, w.now);
|
|
129
|
+
return Number(result.lastInsertRowid ?? 0);
|
|
130
|
+
}
|
|
131
|
+
function supersedeBriefRow(db, w, supersedesId, briefId, version) {
|
|
132
|
+
const sup = db.prepare(`
|
|
133
|
+
UPDATE project_briefs
|
|
134
|
+
SET status = 'superseded', superseded_by = ?, superseded_at = ?
|
|
135
|
+
WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
|
|
136
|
+
`).run(briefId, w.now, supersedesId, w.tenantId, briefId);
|
|
137
|
+
if (sup.changes === 0) {
|
|
138
|
+
throw new ConflictError(`saveProjectBrief: brief ${supersedesId} could not be superseded (no longer active or self-reference).`);
|
|
139
|
+
}
|
|
140
|
+
appendAuditEvent(db, {
|
|
141
|
+
tenantId: w.tenantId,
|
|
142
|
+
actor: w.actor,
|
|
143
|
+
op: 'project_brief_supersede',
|
|
144
|
+
targetId: String(supersedesId),
|
|
145
|
+
metadata: {
|
|
146
|
+
brief_id: supersedesId,
|
|
147
|
+
superseded_by: briefId,
|
|
148
|
+
new_version: version,
|
|
149
|
+
refreshed: w.isRefresh,
|
|
150
|
+
...w.refreshAuditExtra,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** The afterWrite body: preflight, INSERT, supersede, reload, create audit, all in one SAVEPOINT. */
|
|
155
|
+
function writeBriefRow(db, memoryId, w) {
|
|
156
|
+
const version = w.supersedesId !== undefined ? preflightBriefSupersede(db, w.tenantId, w.supersedesId) : 1;
|
|
157
|
+
const briefId = insertBriefRow(db, memoryId, w, version);
|
|
158
|
+
if (w.supersedesId !== undefined)
|
|
159
|
+
supersedeBriefRow(db, w, w.supersedesId, briefId, version);
|
|
160
|
+
// SAFETY: SELECT ${BRIEF_COLS} projects exactly the ProjectBriefRow
|
|
161
|
+
// columns; .get() returns that row, or undefined only if the
|
|
162
|
+
// just-inserted id can't be found.
|
|
163
|
+
const row = db.prepare(`SELECT ${BRIEF_COLS} FROM project_briefs WHERE id = ?`)
|
|
164
|
+
.get(briefId);
|
|
165
|
+
if (!row)
|
|
166
|
+
throw new Error('saveProjectBrief: failed to reload saved brief row');
|
|
167
|
+
// GDPR-light metadata: ids + flags only, no brief text.
|
|
168
|
+
appendAuditEvent(db, {
|
|
169
|
+
tenantId: w.tenantId,
|
|
170
|
+
actor: w.actor,
|
|
171
|
+
op: 'project_brief_create',
|
|
172
|
+
targetId: String(briefId),
|
|
173
|
+
metadata: {
|
|
174
|
+
brief_id: briefId,
|
|
175
|
+
repo: w.repo,
|
|
176
|
+
version,
|
|
177
|
+
refreshed: w.isRefresh,
|
|
178
|
+
...w.refreshAuditExtra,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
return row;
|
|
182
|
+
}
|
|
109
183
|
/**
|
|
110
184
|
* Create a project_brief (or a new version that supersedes an existing one). Writes
|
|
111
185
|
* the memory mirror + the project_briefs row atomically inside writeEntry's
|
|
@@ -133,79 +207,22 @@ export function saveProjectBrief(hippoRoot, tenantId, opts, actor = 'cli') {
|
|
|
133
207
|
baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
|
|
134
208
|
tenantId,
|
|
135
209
|
});
|
|
210
|
+
const w = {
|
|
211
|
+
tenantId,
|
|
212
|
+
actor,
|
|
213
|
+
repo,
|
|
214
|
+
summary: opts.summary,
|
|
215
|
+
changeSummary,
|
|
216
|
+
supersedesId: opts.supersedesBriefId,
|
|
217
|
+
isRefresh,
|
|
218
|
+
refreshAuditExtra,
|
|
219
|
+
now,
|
|
220
|
+
};
|
|
136
221
|
let savedRow;
|
|
137
222
|
writeEntry(hippoRoot, mem, {
|
|
138
223
|
actor,
|
|
139
224
|
afterWrite: (db, memoryId) => {
|
|
140
|
-
|
|
141
|
-
// autoincrement id can never be its own supersede target); read the
|
|
142
|
-
// predecessor version in the same SELECT for server-derived versioning.
|
|
143
|
-
// Mirrors saveSkill / saveProcess (codex P1 2026-05-28).
|
|
144
|
-
let version = 1;
|
|
145
|
-
if (opts.supersedesBriefId !== undefined) {
|
|
146
|
-
// SAFETY: SELECT projects exactly status, version; .get() returns that
|
|
147
|
-
// shape for the matching row, or undefined when no brief/tenant pair matches.
|
|
148
|
-
const pred = db.prepare(`SELECT status, version FROM project_briefs WHERE id = ? AND tenant_id = ?`).get(opts.supersedesBriefId, tenantId);
|
|
149
|
-
if (!pred) {
|
|
150
|
-
throw new NotFoundError(`saveProjectBrief: brief ${opts.supersedesBriefId} to supersede not found for tenant ${tenantId}`);
|
|
151
|
-
}
|
|
152
|
-
if (pred.status !== 'active') {
|
|
153
|
-
throw new ConflictError(`saveProjectBrief: brief ${opts.supersedesBriefId} is not active (status='${pred.status}'); only active briefs can be superseded.`);
|
|
154
|
-
}
|
|
155
|
-
version = pred.version + 1;
|
|
156
|
-
}
|
|
157
|
-
const result = db.prepare(`
|
|
158
|
-
INSERT INTO project_briefs(
|
|
159
|
-
memory_id, tenant_id, repo, summary, version,
|
|
160
|
-
status, superseded_by, superseded_at, change_summary, closed_at, created_at
|
|
161
|
-
) VALUES (?, ?, ?, ?, ?, 'active', NULL, NULL, ?, NULL, ?)
|
|
162
|
-
`).run(memoryId, tenantId, repo, opts.summary, version, changeSummary, now);
|
|
163
|
-
const briefId = Number(result.lastInsertRowid ?? 0);
|
|
164
|
-
if (opts.supersedesBriefId !== undefined) {
|
|
165
|
-
const sup = db.prepare(`
|
|
166
|
-
UPDATE project_briefs
|
|
167
|
-
SET status = 'superseded', superseded_by = ?, superseded_at = ?
|
|
168
|
-
WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
|
|
169
|
-
`).run(briefId, now, opts.supersedesBriefId, tenantId, briefId);
|
|
170
|
-
if (sup.changes === 0) {
|
|
171
|
-
throw new ConflictError(`saveProjectBrief: brief ${opts.supersedesBriefId} could not be superseded (no longer active or self-reference).`);
|
|
172
|
-
}
|
|
173
|
-
appendAuditEvent(db, {
|
|
174
|
-
tenantId,
|
|
175
|
-
actor,
|
|
176
|
-
op: 'project_brief_supersede',
|
|
177
|
-
targetId: String(opts.supersedesBriefId),
|
|
178
|
-
metadata: {
|
|
179
|
-
brief_id: opts.supersedesBriefId,
|
|
180
|
-
superseded_by: briefId,
|
|
181
|
-
new_version: version,
|
|
182
|
-
refreshed: isRefresh,
|
|
183
|
-
...refreshAuditExtra,
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
// SAFETY: SELECT ${BRIEF_COLS} projects exactly the ProjectBriefRow
|
|
188
|
-
// columns; .get() returns that row, or undefined only if the
|
|
189
|
-
// just-inserted id can't be found.
|
|
190
|
-
const row = db.prepare(`SELECT ${BRIEF_COLS} FROM project_briefs WHERE id = ?`)
|
|
191
|
-
.get(briefId);
|
|
192
|
-
if (!row)
|
|
193
|
-
throw new Error('saveProjectBrief: failed to reload saved brief row');
|
|
194
|
-
savedRow = row;
|
|
195
|
-
// GDPR-light metadata: ids + flags only, no brief text.
|
|
196
|
-
appendAuditEvent(db, {
|
|
197
|
-
tenantId,
|
|
198
|
-
actor,
|
|
199
|
-
op: 'project_brief_create',
|
|
200
|
-
targetId: String(briefId),
|
|
201
|
-
metadata: {
|
|
202
|
-
brief_id: briefId,
|
|
203
|
-
repo,
|
|
204
|
-
version,
|
|
205
|
-
refreshed: isRefresh,
|
|
206
|
-
...refreshAuditExtra,
|
|
207
|
-
},
|
|
208
|
-
});
|
|
225
|
+
savedRow = writeBriefRow(db, memoryId, w);
|
|
209
226
|
},
|
|
210
227
|
// afterCommit covers refreshBrief (delegates here) + brief new/supersede.
|
|
211
228
|
afterCommit: () => markGraphDirty(hippoRoot, tenantId, mem.id),
|
|
@@ -259,7 +276,7 @@ export function closeProjectBrief(hippoRoot, tenantId, id, actor = 'cli') {
|
|
|
259
276
|
// Closing removes the object from the graph. Remove its rows DIRECTLY (deterministic),
|
|
260
277
|
// not only via an enqueued rebuild whose queue item is lost if the mirror is later
|
|
261
278
|
// forgotten (the queue row cascade-deletes with the memory), which would leave the closed
|
|
262
|
-
// object stale and could block that forget
|
|
279
|
+
// object stale and could block that forget. Still enqueue when a mirror exists
|
|
263
280
|
// so a concurrent rebuild re-derives consistently (harmless if it also runs).
|
|
264
281
|
removeGraphEntitiesForObject(hippoRoot, tenantId, 'project', closed.id);
|
|
265
282
|
if (closed.memoryId) {
|
|
@@ -334,7 +351,7 @@ export function loadProjectBriefs(hippoRoot, tenantId, opts = {}) {
|
|
|
334
351
|
/**
|
|
335
352
|
* The repo's CURRENT active brief, or null. By convention there is one active brief
|
|
336
353
|
* per (tenant, repo); if an operator created more than one (the DB does not prevent
|
|
337
|
-
* it, consistent with every other
|
|
354
|
+
* it, consistent with every other first-class object), the MOST-RECENT active row wins.
|
|
338
355
|
*/
|
|
339
356
|
export function loadActiveBriefForRepo(hippoRoot, tenantId, repo) {
|
|
340
357
|
assertTenantId('loadActiveBriefForRepo', tenantId);
|
|
@@ -358,10 +375,6 @@ export function loadActiveBriefForRepo(hippoRoot, tenantId, repo) {
|
|
|
358
375
|
// ---------------------------------------------------------------------------
|
|
359
376
|
// Refresh assembler (the distinguishing deliverable)
|
|
360
377
|
// ---------------------------------------------------------------------------
|
|
361
|
-
/** Escape LIKE wildcards in operator-supplied text (mirror of store/search-rows.ts). */
|
|
362
|
-
function escapeLike(term) {
|
|
363
|
-
return term.replace(/[%_\\]/g, '\\$&');
|
|
364
|
-
}
|
|
365
378
|
/** Single-line headline for a receipt: first non-empty line, newline-stripped,
|
|
366
379
|
* truncated. Deterministic + safe for the markdown bullet list. */
|
|
367
380
|
function receiptHeadline(content) {
|
|
@@ -371,34 +384,16 @@ function receiptHeadline(content) {
|
|
|
371
384
|
? `${trimmed.slice(0, MAX_RECEIPT_HEADLINE_LEN)}...`
|
|
372
385
|
: trimmed;
|
|
373
386
|
}
|
|
374
|
-
/**
|
|
375
|
-
|
|
376
|
-
* return it WITH the receipt count (the count feeds refreshBrief's change_summary +
|
|
377
|
-
* audit metadata). NO LLM. Always returns a non-empty, valid summary (a brief
|
|
378
|
-
* `summary` is NOT NULL), including the zero-receipts case.
|
|
379
|
-
*
|
|
380
|
-
* A "receipt" = a tenant memory row carrying the repo's `path:<repo>` tag. The
|
|
381
|
-
* brief's OWN memory mirror (source='project_brief') is excluded so a brief never
|
|
382
|
-
* becomes its own receipt on the next refresh. The match is against the JSON-array
|
|
383
|
-
* serialization (each element is a double-quoted string `"path:hippo"`); the
|
|
384
|
-
* surrounding quotes are load-bearing — they stop `hip` matching `path:hippo`.
|
|
385
|
-
* `repo` is LIKE-escaped + parameterized (operator-supplied; security.md).
|
|
386
|
-
*/
|
|
387
|
-
export function assembleBriefFromReceipts(hippoRoot, tenantId, repo) {
|
|
388
|
-
assertTenantId('assembleBriefFromReceipts', tenantId);
|
|
389
|
-
const normalizedRepo = (repo ?? '').trim();
|
|
390
|
-
if (normalizedRepo.length === 0) {
|
|
391
|
-
throw new BadRequestError('assembleBriefFromReceipts: repo is required');
|
|
392
|
-
}
|
|
387
|
+
/** The repo's receipt rows, newest first, capped at MAX_BRIEF_RECEIPTS. */
|
|
388
|
+
function loadBriefReceipts(hippoRoot, tenantId, normalizedRepo) {
|
|
393
389
|
const tag = `path:${normalizedRepo.toLowerCase()}`;
|
|
394
390
|
const likeParam = `%"${escapeLike(tag)}"%`;
|
|
395
391
|
const denyPlaceholders = RECALL_DEFAULT_DENY_SCOPES.map(() => '?').join(', ');
|
|
396
392
|
const db = openHippoDb(hippoRoot);
|
|
397
|
-
let receipts;
|
|
398
393
|
try {
|
|
399
394
|
// SAFETY: SELECT projects exactly id, created, source, content (the
|
|
400
395
|
// ReceiptRow columns); .all() returns rows in that shape.
|
|
401
|
-
|
|
396
|
+
return db.prepare(`
|
|
402
397
|
SELECT id, created, source, content FROM memories
|
|
403
398
|
WHERE tenant_id = ?
|
|
404
399
|
AND source != 'project_brief'
|
|
@@ -411,17 +406,19 @@ export function assembleBriefFromReceipts(hippoRoot, tenantId, repo) {
|
|
|
411
406
|
finally {
|
|
412
407
|
closeHippoDb(db);
|
|
413
408
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
409
|
+
}
|
|
410
|
+
// NOTE on ordering: the `id DESC` tiebreak is lexical on a random-ish memory id
|
|
411
|
+
// (e.g. `sem_<hex>`), NOT chronological — within the same `created` timestamp the
|
|
412
|
+
// order is stable-but-arbitrary, not insertion order. `created DESC` is the real
|
|
413
|
+
// recency ordering.
|
|
414
|
+
//
|
|
415
|
+
// Budget-aware assembly: the digest is the
|
|
416
|
+
// brief `summary`, which saveProjectBrief caps at MAX_BRIEF_SUMMARY_LEN. The
|
|
417
|
+
// receipt/headline caps (50 x ~200) could otherwise build an ~11KB body that the
|
|
418
|
+
// store then REJECTS, breaking refresh for inputs within the advertised caps. So
|
|
419
|
+
// include receipt lines newest-first only while they fit under the cap (reserving
|
|
420
|
+
// slack for the header + an omission footer), and note the omitted remainder.
|
|
421
|
+
function fitReceiptLines(receipts) {
|
|
425
422
|
const buildReceiptLine = (r) => `- ${(r.created ?? '').slice(0, 10)} [${r.source}] ${receiptHeadline(r.content)}`;
|
|
426
423
|
const receiptLines = [];
|
|
427
424
|
if (receipts.length > 0) {
|
|
@@ -437,17 +434,20 @@ export function assembleBriefFromReceipts(hippoRoot, tenantId, repo) {
|
|
|
437
434
|
bodyBudget -= line.length + 1;
|
|
438
435
|
}
|
|
439
436
|
}
|
|
440
|
-
|
|
437
|
+
return receiptLines;
|
|
438
|
+
}
|
|
439
|
+
function renderBriefDigest(normalizedRepo, receiptCount, receiptLines) {
|
|
440
|
+
const omitted = receiptCount - receiptLines.length;
|
|
441
441
|
const lines = [];
|
|
442
442
|
lines.push(`# Project Brief: ${normalizedRepo}`);
|
|
443
443
|
lines.push('');
|
|
444
444
|
lines.push(omitted > 0
|
|
445
|
-
? `_Auto-assembled from ${receiptLines.length} of ${
|
|
446
|
-
: `_Auto-assembled from ${
|
|
445
|
+
? `_Auto-assembled from ${receiptLines.length} of ${receiptCount} receipt(s)._`
|
|
446
|
+
: `_Auto-assembled from ${receiptCount} receipt(s)._`);
|
|
447
447
|
lines.push('');
|
|
448
448
|
lines.push('## Recent receipts');
|
|
449
449
|
lines.push('');
|
|
450
|
-
if (
|
|
450
|
+
if (receiptCount === 0) {
|
|
451
451
|
lines.push(`_No receipts found for ${normalizedRepo}._`);
|
|
452
452
|
}
|
|
453
453
|
else {
|
|
@@ -464,6 +464,29 @@ export function assembleBriefFromReceipts(hippoRoot, tenantId, repo) {
|
|
|
464
464
|
if (markdown.length > MAX_BRIEF_SUMMARY_LEN) {
|
|
465
465
|
markdown = markdown.slice(0, MAX_BRIEF_SUMMARY_LEN);
|
|
466
466
|
}
|
|
467
|
+
return markdown;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Assemble the repo's recent receipts into a deterministic markdown digest, and
|
|
471
|
+
* return it WITH the receipt count (the count feeds refreshBrief's change_summary +
|
|
472
|
+
* audit metadata). NO LLM. Always returns a non-empty, valid summary (a brief
|
|
473
|
+
* `summary` is NOT NULL), including the zero-receipts case.
|
|
474
|
+
*
|
|
475
|
+
* A "receipt" = a tenant memory row carrying the repo's `path:<repo>` tag. The
|
|
476
|
+
* brief's OWN memory mirror (source='project_brief') is excluded so a brief never
|
|
477
|
+
* becomes its own receipt on the next refresh. The match is against the JSON-array
|
|
478
|
+
* serialization (each element is a double-quoted string `"path:hippo"`); the
|
|
479
|
+
* surrounding quotes are load-bearing — they stop `hip` matching `path:hippo`.
|
|
480
|
+
* `repo` is LIKE-escaped + parameterized (operator-supplied; security.md).
|
|
481
|
+
*/
|
|
482
|
+
export function assembleBriefFromReceipts(hippoRoot, tenantId, repo) {
|
|
483
|
+
assertTenantId('assembleBriefFromReceipts', tenantId);
|
|
484
|
+
const normalizedRepo = (repo ?? '').trim();
|
|
485
|
+
if (normalizedRepo.length === 0) {
|
|
486
|
+
throw new BadRequestError('assembleBriefFromReceipts: repo is required');
|
|
487
|
+
}
|
|
488
|
+
const receipts = loadBriefReceipts(hippoRoot, tenantId, normalizedRepo);
|
|
489
|
+
const markdown = renderBriefDigest(normalizedRepo, receipts.length, fitReceiptLines(receipts));
|
|
467
490
|
return { markdown, receiptCount: receipts.length };
|
|
468
491
|
}
|
|
469
492
|
/**
|
|
@@ -491,8 +514,8 @@ export function refreshBrief(hippoRoot, tenantId, repo, actor = 'cli') {
|
|
|
491
514
|
supersedesBriefId: active ? active.id : undefined,
|
|
492
515
|
refreshReceiptCount: receiptCount,
|
|
493
516
|
// Tag the refreshed brief's mirror as repo-local so path-aware recall boosts
|
|
494
|
-
// it like the manual `brief new`/`supersede` paths do
|
|
495
|
-
//
|
|
517
|
+
// it like the manual `brief new`/`supersede` paths do.
|
|
518
|
+
// Safe vs self-recursion: assembleBriefFromReceipts excludes
|
|
496
519
|
// source='project_brief', so the brief never becomes its own receipt.
|
|
497
520
|
extraTags: [`path:${normalizedRepo.toLowerCase()}`],
|
|
498
521
|
}, actor);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project identity resolution for memory scope isolation
|
|
3
|
-
* [Committed] "Memory scope isolation"; plan docs/plans/2026-07-01-memory-scope-isolation.md S1).
|
|
2
|
+
* Project identity resolution for memory scope isolation.
|
|
4
3
|
*
|
|
5
4
|
* Resolution rules:
|
|
6
5
|
* - The nearest ancestor of cwd (including cwd itself) containing a `.hippo`
|
|
@@ -11,6 +10,9 @@
|
|
|
11
10
|
* - A directory with no marker anywhere up the walk is NOT a project: it
|
|
12
11
|
* resolves to the user-global identity (empty name), so memories written
|
|
13
12
|
* there stay injectable everywhere (matches pre-isolation behavior).
|
|
13
|
+
* - A project's name is its id: `.hippo-project.json` at the root, else the
|
|
14
|
+
* checkout's normalised `origin` remote, else the folder rule (legacyName),
|
|
15
|
+
* so two repos in folders of one name stay apart.
|
|
14
16
|
*
|
|
15
17
|
* NOTE: this module must stay free of imports from shared.ts / store.ts /
|
|
16
18
|
* api.ts so any of them can import it without creating a cycle.
|
|
@@ -19,8 +21,12 @@
|
|
|
19
21
|
export interface ProjectIdentity {
|
|
20
22
|
/** Realpath-resolved root directory of the project (the start dir when not in a project). */
|
|
21
23
|
root: string;
|
|
22
|
-
/**
|
|
24
|
+
/** The project id rows are stamped with: the project file's id, the origin remote, or legacyName; '' outside a project. */
|
|
23
25
|
name: string;
|
|
26
|
+
/** The folder rule rows written before ids carry: the root's lowercased basename, or its repo's for a linked worktree with no `.hippo`. */
|
|
27
|
+
legacyName: string;
|
|
28
|
+
/** Every other name this root resolves to (the project file id, the origin remote even with the rule off, legacyName), so rows written under an earlier id stay readable. */
|
|
29
|
+
aliases?: readonly string[];
|
|
24
30
|
/** True when the directory resolves to the user home working set. */
|
|
25
31
|
isHome: boolean;
|
|
26
32
|
}
|
|
@@ -33,7 +39,7 @@ export interface ResolveProjectIdentityOpts {
|
|
|
33
39
|
homeDir?: string;
|
|
34
40
|
stopDir?: string;
|
|
35
41
|
}
|
|
36
|
-
/** Clear the per-process identity cache (test seam). */
|
|
42
|
+
/** Clear the per-process identity cache and the remote switch it read (test seam). */
|
|
37
43
|
export declare function clearProjectIdentityCache(): void;
|
|
38
44
|
/**
|
|
39
45
|
* Canonicalize a path via realpath, falling back to path.resolve when the
|
|
@@ -46,17 +52,25 @@ export declare function realpathOrResolve(p: string): string;
|
|
|
46
52
|
*/
|
|
47
53
|
export declare function resolveProjectIdentity(cwd?: string, opts?: ResolveProjectIdentityOpts): ProjectIdentity;
|
|
48
54
|
/** Nearest ancestor `.hippo` below home and the temp root (never projects; on Windows the temp root sits inside home).
|
|
49
|
-
* Everything is realpath'd so a symlinked temp root or cwd still matches its bound.
|
|
55
|
+
* Everything is realpath'd so a symlinked temp root or cwd still matches its bound. */
|
|
50
56
|
export declare function findHippoStoreDir(cwd?: string, opts?: ResolveProjectIdentityOpts): string | null;
|
|
57
|
+
/** A reader's project: a bare name, or an identity whose own rows may also carry its legacy folder name. */
|
|
58
|
+
export type ProjectRef = string | Pick<ProjectIdentity, 'name' | 'legacyName' | 'aliases'>;
|
|
59
|
+
/** The id a reader's new rows are stamped with; '' outside a project. */
|
|
60
|
+
export declare function projectId(project: ProjectRef): string;
|
|
61
|
+
/** Every origin_project value the reader's own rows carry: the id first, then each alias and the legacy name. */
|
|
62
|
+
export declare function projectNames(project: ProjectRef): readonly string[];
|
|
63
|
+
/** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
|
|
64
|
+
export declare function originInSql(names: readonly string[], column?: string): string;
|
|
51
65
|
/**
|
|
52
66
|
* v39 memory scope isolation: classify a memory's origin_project against the
|
|
53
|
-
* active project.
|
|
67
|
+
* active project. An empty current id means the session is not in a project
|
|
54
68
|
* (home dir or markerless cwd) - everything is in scope there, matching
|
|
55
69
|
* pre-isolation behavior. NULL/undefined origin is a legacy pre-v39 row and
|
|
56
70
|
* is treated as cross-project (deny by default) - the safe direction for a
|
|
57
71
|
* security partition.
|
|
58
72
|
*/
|
|
59
|
-
export declare function classifyOriginProject(origin: string | null | undefined,
|
|
73
|
+
export declare function classifyOriginProject(origin: string | null | undefined, current: ProjectRef): 'project' | 'user-global' | 'cross-project';
|
|
60
74
|
/**
|
|
61
75
|
* The global Hippo store directory, resolved the same way shared.ts does:
|
|
62
76
|
* $HIPPO_HOME > $XDG_DATA_HOME/hippo > ~/.hippo. Lives here (leaf module) so
|
|
@@ -85,8 +99,7 @@ export declare function originFromSource(source: string | null | undefined, home
|
|
|
85
99
|
* Returns the project name, or '' for user-global (written at/under home or
|
|
86
100
|
* in a markerless directory) - injectable everywhere. Write sites must always
|
|
87
101
|
* persist this value; a NULL origin_project column is reserved for legacy
|
|
88
|
-
* pre-migration rows, which ambient context treats as deny
|
|
89
|
-
* docs/plans/2026-07-01-memory-scope-isolation.md "Origin model").
|
|
102
|
+
* pre-migration rows, which ambient context treats as deny.
|
|
90
103
|
*/
|
|
91
104
|
export declare function deriveOriginProject(cwd?: string, opts?: ResolveProjectIdentityOpts): string;
|
|
92
105
|
//# sourceMappingURL=project-identity.d.ts.map
|
package/dist/project-identity.js
CHANGED
|
@@ -2,12 +2,31 @@ import { envHippoHome, envXdgDataHome } from './env.js';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
5
6
|
import { log } from './log.js';
|
|
7
|
+
import { originRemoteId, projectFileId } from './project-remote.js';
|
|
6
8
|
const MAX_WALK_DEPTH = 64;
|
|
7
9
|
const identityCache = new Map();
|
|
8
|
-
|
|
10
|
+
let remoteSwitch = null;
|
|
11
|
+
/** Clear the per-process identity cache and the remote switch it read (test seam). */
|
|
9
12
|
export function clearProjectIdentityCache() {
|
|
10
13
|
identityCache.clear();
|
|
14
|
+
remoteSwitch = null;
|
|
15
|
+
}
|
|
16
|
+
// The global store's config only: a per-store switch would stamp one checkout two ways.
|
|
17
|
+
function remoteRuleOn() {
|
|
18
|
+
const globalRoot = resolveGlobalRootDir();
|
|
19
|
+
if (remoteSwitch?.globalRoot !== globalRoot)
|
|
20
|
+
remoteSwitch = { globalRoot, on: loadConfig(globalRoot).projectIdentity.remote };
|
|
21
|
+
return remoteSwitch.on;
|
|
22
|
+
}
|
|
23
|
+
/** The id a root names itself by plus every rung that resolves; only the root's own `.git` is read, so a store nested in another checkout keeps its folder name. */
|
|
24
|
+
function namesAt(root, legacyName) {
|
|
25
|
+
const fileId = projectFileId(root);
|
|
26
|
+
const remoteId = originRemoteId(root);
|
|
27
|
+
const name = fileId ?? (remoteRuleOn() ? remoteId : null) ?? legacyName;
|
|
28
|
+
const aliases = [...new Set([fileId, remoteId, legacyName])].filter((n) => n !== null && n !== name);
|
|
29
|
+
return aliases.length > 0 ? { name, aliases } : { name };
|
|
11
30
|
}
|
|
12
31
|
/**
|
|
13
32
|
* Canonicalize a path via realpath, falling back to path.resolve when the
|
|
@@ -72,16 +91,16 @@ export function resolveProjectIdentity(cwd, opts) {
|
|
|
72
91
|
let identity;
|
|
73
92
|
const root = hippoRoot ?? gitRoot;
|
|
74
93
|
if (root !== null) {
|
|
75
|
-
const
|
|
76
|
-
identity = { root,
|
|
94
|
+
const legacyName = (hippoRoot === null ? linkedWorktreeRepoName(root, home) : null) ?? path.basename(root).toLowerCase();
|
|
95
|
+
identity = { root, ...namesAt(root, legacyName), legacyName, isHome: false };
|
|
77
96
|
}
|
|
78
97
|
else if (reachedHome || isUnder(start, home)) {
|
|
79
|
-
identity = { root: home, name: '', isHome: true };
|
|
98
|
+
identity = { root: home, name: '', legacyName: '', isHome: true };
|
|
80
99
|
}
|
|
81
100
|
else {
|
|
82
101
|
// No markers anywhere: not a project. Empty name keeps these memories
|
|
83
102
|
// user-global rather than fabricating an origin from a basename.
|
|
84
|
-
identity = { root: start, name: '', isHome: false };
|
|
103
|
+
identity = { root: start, name: '', legacyName: '', isHome: false };
|
|
85
104
|
}
|
|
86
105
|
if (cacheable)
|
|
87
106
|
identityCache.set(startInput, identity);
|
|
@@ -137,7 +156,7 @@ function walkProjectMarkers(start, home, stopDirs) {
|
|
|
137
156
|
return { hippoRoot, gitRoot, reachedHome };
|
|
138
157
|
}
|
|
139
158
|
/** Nearest ancestor `.hippo` below home and the temp root (never projects; on Windows the temp root sits inside home).
|
|
140
|
-
* Everything is realpath'd so a symlinked temp root or cwd still matches its bound.
|
|
159
|
+
* Everything is realpath'd so a symlinked temp root or cwd still matches its bound. */
|
|
141
160
|
export function findHippoStoreDir(cwd, opts) {
|
|
142
161
|
const home = realpathOrResolve(opts?.homeDir ?? os.homedir());
|
|
143
162
|
const stops = [realpathOrResolve(os.tmpdir())];
|
|
@@ -147,22 +166,39 @@ export function findHippoStoreDir(cwd, opts) {
|
|
|
147
166
|
const { hippoRoot } = walkProjectMarkers(start, home, stops);
|
|
148
167
|
return hippoRoot === null ? null : path.join(hippoRoot, '.hippo');
|
|
149
168
|
}
|
|
169
|
+
function isBareName(project) {
|
|
170
|
+
return typeof project === 'string';
|
|
171
|
+
}
|
|
172
|
+
/** The id a reader's new rows are stamped with; '' outside a project. */
|
|
173
|
+
export function projectId(project) {
|
|
174
|
+
return isBareName(project) ? project : project.name;
|
|
175
|
+
}
|
|
176
|
+
/** Every origin_project value the reader's own rows carry: the id first, then each alias and the legacy name. */
|
|
177
|
+
export function projectNames(project) {
|
|
178
|
+
if (isBareName(project) || project.name === '')
|
|
179
|
+
return [projectId(project)];
|
|
180
|
+
return [...new Set([project.name, ...(project.aliases ?? []), project.legacyName])].filter((n) => n !== '');
|
|
181
|
+
}
|
|
182
|
+
/** `origin_project IN (?, ...)` with one placeholder per name; an empty list matches nothing. */
|
|
183
|
+
export function originInSql(names, column = 'origin_project') {
|
|
184
|
+
return names.length === 0 ? '0' : `${column} IN (${names.map(() => '?').join(', ')})`;
|
|
185
|
+
}
|
|
150
186
|
/**
|
|
151
187
|
* v39 memory scope isolation: classify a memory's origin_project against the
|
|
152
|
-
* active project.
|
|
188
|
+
* active project. An empty current id means the session is not in a project
|
|
153
189
|
* (home dir or markerless cwd) - everything is in scope there, matching
|
|
154
190
|
* pre-isolation behavior. NULL/undefined origin is a legacy pre-v39 row and
|
|
155
191
|
* is treated as cross-project (deny by default) - the safe direction for a
|
|
156
192
|
* security partition.
|
|
157
193
|
*/
|
|
158
|
-
export function classifyOriginProject(origin,
|
|
159
|
-
if (
|
|
194
|
+
export function classifyOriginProject(origin, current) {
|
|
195
|
+
if (projectId(current) === '')
|
|
160
196
|
return 'project';
|
|
161
197
|
if (origin === undefined || origin === null)
|
|
162
198
|
return 'cross-project';
|
|
163
199
|
if (origin === '')
|
|
164
200
|
return 'user-global';
|
|
165
|
-
return origin
|
|
201
|
+
return projectNames(current).includes(origin) ? 'project' : 'cross-project';
|
|
166
202
|
}
|
|
167
203
|
/**
|
|
168
204
|
* The global Hippo store directory, resolved the same way shared.ts does:
|
|
@@ -221,8 +257,7 @@ export function originFromSource(source, homeName) {
|
|
|
221
257
|
* Returns the project name, or '' for user-global (written at/under home or
|
|
222
258
|
* in a markerless directory) - injectable everywhere. Write sites must always
|
|
223
259
|
* persist this value; a NULL origin_project column is reserved for legacy
|
|
224
|
-
* pre-migration rows, which ambient context treats as deny
|
|
225
|
-
* docs/plans/2026-07-01-memory-scope-isolation.md "Origin model").
|
|
260
|
+
* pre-migration rows, which ambient context treats as deny.
|
|
226
261
|
*/
|
|
227
262
|
export function deriveOriginProject(cwd, opts) {
|
|
228
263
|
return resolveProjectIdentity(cwd, opts).name;
|
package/dist/project-merge.d.ts
CHANGED
|
@@ -11,14 +11,29 @@ export interface ProjectSummary {
|
|
|
11
11
|
export interface MergeResult {
|
|
12
12
|
readonly from: string;
|
|
13
13
|
readonly into: string;
|
|
14
|
-
/**
|
|
14
|
+
/** Imports `into` already holds for the same note, moved to dormant storage; the rest are re-tagged and the next sync files them under `into`. */
|
|
15
15
|
readonly setAside: readonly string[];
|
|
16
16
|
readonly restamped: readonly string[];
|
|
17
17
|
readonly dormantRestamped: readonly string[];
|
|
18
18
|
readonly compactions: number;
|
|
19
19
|
readonly backup: string | null;
|
|
20
20
|
}
|
|
21
|
+
export interface ProjectFold {
|
|
22
|
+
readonly from: string;
|
|
23
|
+
readonly into: string;
|
|
24
|
+
}
|
|
25
|
+
/** An old name whose session folders now resolve to several projects: no fold can say whose rows are whose. */
|
|
26
|
+
export interface ProjectCollision {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly ids: readonly string[];
|
|
29
|
+
}
|
|
21
30
|
export interface RepairResult {
|
|
31
|
+
/** Imported notes filed under the wrong project, or under a project name when a user-global import holds the same text: set aside. */
|
|
32
|
+
readonly copies: readonly string[];
|
|
33
|
+
/** Names whose recorded session folders all resolve to one other project today, folded as `merge` would. */
|
|
34
|
+
readonly folds: readonly ProjectFold[];
|
|
35
|
+
/** Names left unfolded because their folders now resolve to more than one project; only `merge` by hand can split them. */
|
|
36
|
+
readonly collisions: readonly ProjectCollision[];
|
|
22
37
|
readonly toProject: ReadonlyArray<{
|
|
23
38
|
readonly id: string;
|
|
24
39
|
readonly origin: string;
|
|
@@ -42,11 +57,19 @@ export declare function mergeProjects(db: DatabaseSyncLike, hippoRoot: string, o
|
|
|
42
57
|
into: string;
|
|
43
58
|
dryRun: boolean;
|
|
44
59
|
}): MergeResult;
|
|
45
|
-
/**
|
|
46
|
-
export declare function
|
|
47
|
-
/**
|
|
48
|
-
export declare function
|
|
60
|
+
/** Names folded, directly or through others, into one of `names`; the sync moves imports still filed under them. */
|
|
61
|
+
export declare function namesFoldedInto(db: DatabaseSyncLike, tenantId: string, names: readonly string[]): string[];
|
|
62
|
+
/** Reads only, so doctor and a dry run take no write lock; merged rows are planned before any fold, so a few may re-tag differently once folds apply. */
|
|
63
|
+
export declare function planProjectRepair(db: DatabaseSyncLike, hippoRoot: string, tenantId: string, globalFolds?: boolean): Omit<RepairResult, 'backup'>;
|
|
64
|
+
/** Sets aside stray imports, folds the names the resolver now maps elsewhere, then re-tags sleep's user-global merges by their parents; a dry run only plans. */
|
|
65
|
+
export declare function repairProjects(db: DatabaseSyncLike, hippoRoot: string, opts: {
|
|
49
66
|
tenantId: string;
|
|
50
67
|
dryRun: boolean;
|
|
68
|
+
globalFolds?: boolean;
|
|
51
69
|
}): RepairResult;
|
|
70
|
+
/**
|
|
71
|
+
* Sleep runs repair once per store, so an upgrade needs no command. Global-store folds stay
|
|
72
|
+
* with `hippo projects repair`: their evidence is compaction folders, blind to a same-named repo that never compacted.
|
|
73
|
+
*/
|
|
74
|
+
export declare function repairOnceOnSleep(db: DatabaseSyncLike, hippoRoot: string, tenantId: string): RepairResult | null;
|
|
52
75
|
//# sourceMappingURL=project-merge.d.ts.map
|