superdev-cli 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.
Files changed (182) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +21 -0
  3. package/.codex-plugin/plugin.json +27 -0
  4. package/CODE_OF_CONDUCT.md +109 -0
  5. package/CONTRIBUTING.md +205 -0
  6. package/LICENSE +202 -0
  7. package/NOTICE +11 -0
  8. package/README.md +1051 -0
  9. package/SECURITY.md +135 -0
  10. package/THIRD-PARTY-NOTICES.md +253 -0
  11. package/hooks/hooks.json +61 -0
  12. package/hooks/run.mjs +88 -0
  13. package/package.json +65 -0
  14. package/references/confidentiality.md +48 -0
  15. package/references/evidence-and-risk.md +73 -0
  16. package/references/operating-model.md +105 -0
  17. package/references/platform-capabilities.md +51 -0
  18. package/references/project-record.md +91 -0
  19. package/references/provider-contracts.md +102 -0
  20. package/scripts/doctor/doctor.mjs +539 -0
  21. package/scripts/privacy/scan-history.mjs +163 -0
  22. package/scripts/privacy/scan.mjs +368 -0
  23. package/scripts/validate/README.md +94 -0
  24. package/scripts/validate/common.mjs +149 -0
  25. package/scripts/validate/data-model.mjs +225 -0
  26. package/scripts/validate/dependencies.mjs +69 -0
  27. package/scripts/validate/docs-templates.mjs +99 -0
  28. package/scripts/validate/footprint.mjs +78 -0
  29. package/scripts/validate/imports.mjs +61 -0
  30. package/scripts/validate/manifests.mjs +173 -0
  31. package/scripts/validate/markdown.mjs +123 -0
  32. package/scripts/validate/migrations.mjs +187 -0
  33. package/scripts/validate/no-tests.mjs +136 -0
  34. package/scripts/validate/privacy.mjs +115 -0
  35. package/scripts/validate/record-links.mjs +127 -0
  36. package/scripts/validate/skill-commands.mjs +190 -0
  37. package/scripts/validate/skills.mjs +112 -0
  38. package/scripts/validate/specification.mjs +88 -0
  39. package/scripts/validate/style.mjs +78 -0
  40. package/scripts/validate/validate-all.mjs +175 -0
  41. package/skills/debug/SKILL.md +68 -0
  42. package/skills/decision/SKILL.md +99 -0
  43. package/skills/docs/SKILL.md +86 -0
  44. package/skills/docs/assets/fragments/api/events.md +14 -0
  45. package/skills/docs/assets/fragments/api/graphql.md +14 -0
  46. package/skills/docs/assets/fragments/api/local-only.md +13 -0
  47. package/skills/docs/assets/fragments/api/rest.md +13 -0
  48. package/skills/docs/assets/fragments/api/rpc.md +13 -0
  49. package/skills/docs/assets/fragments/async/event-bus.md +13 -0
  50. package/skills/docs/assets/fragments/async/none.md +11 -0
  51. package/skills/docs/assets/fragments/async/platform-jobs.md +13 -0
  52. package/skills/docs/assets/fragments/async/queue.md +13 -0
  53. package/skills/docs/assets/fragments/async/scheduler.md +13 -0
  54. package/skills/docs/assets/fragments/auth/detected-provider.md +15 -0
  55. package/skills/docs/assets/fragments/auth/neutral-base.md +14 -0
  56. package/skills/docs/assets/fragments/data/document.md +13 -0
  57. package/skills/docs/assets/fragments/data/external-saas.md +13 -0
  58. package/skills/docs/assets/fragments/data/key-value.md +13 -0
  59. package/skills/docs/assets/fragments/data/none.md +12 -0
  60. package/skills/docs/assets/fragments/data/sql-orm.md +13 -0
  61. package/skills/docs/assets/fragments/data/sql-plain.md +13 -0
  62. package/skills/docs/assets/fragments/env/conventional.md +13 -0
  63. package/skills/docs/assets/fragments/env/envx.md +13 -0
  64. package/skills/docs/assets/fragments/env/managed-platform.md +13 -0
  65. package/skills/docs/assets/fragments/env/none.md +11 -0
  66. package/skills/docs/assets/fragments/ui/api-only.md +13 -0
  67. package/skills/docs/assets/fragments/ui/cli.md +14 -0
  68. package/skills/docs/assets/fragments/ui/desktop.md +14 -0
  69. package/skills/docs/assets/fragments/ui/mobile.md +14 -0
  70. package/skills/docs/assets/fragments/ui/web.md +14 -0
  71. package/skills/docs/assets/templates/adr.md +62 -0
  72. package/skills/docs/assets/templates/api.md +30 -0
  73. package/skills/docs/assets/templates/architecture.md +52 -0
  74. package/skills/docs/assets/templates/change-impact-drift-report.md +29 -0
  75. package/skills/docs/assets/templates/compliance.md +29 -0
  76. package/skills/docs/assets/templates/data-schema.md +43 -0
  77. package/skills/docs/assets/templates/feature.md +43 -0
  78. package/skills/docs/assets/templates/foundations.md +55 -0
  79. package/skills/docs/assets/templates/jobs-webhooks.md +36 -0
  80. package/skills/docs/assets/templates/module-inventory.md +19 -0
  81. package/skills/docs/assets/templates/module.md +50 -0
  82. package/skills/docs/assets/templates/nfr.md +22 -0
  83. package/skills/docs/assets/templates/observability.md +28 -0
  84. package/skills/docs/assets/templates/pages-ui-actions.md +47 -0
  85. package/skills/docs/assets/templates/project-summary.md +44 -0
  86. package/skills/docs/assets/templates/roles-permissions.md +26 -0
  87. package/skills/docs/assets/templates/test-plan.md +23 -0
  88. package/skills/docs/assets/templates/workflow-state-machine.md +43 -0
  89. package/skills/docs/references/adr-authoring.md +24 -0
  90. package/skills/docs/references/apis-and-data.md +23 -0
  91. package/skills/docs/references/capability-fragments.md +25 -0
  92. package/skills/docs/references/change-tracking.md +62 -0
  93. package/skills/docs/references/diagrams.md +31 -0
  94. package/skills/docs/references/discovery.md +59 -0
  95. package/skills/docs/references/edge-cases.md +45 -0
  96. package/skills/docs/references/foundations-modules.md +18 -0
  97. package/skills/docs/references/ingestion.md +52 -0
  98. package/skills/docs/references/initialize-adopt.md +25 -0
  99. package/skills/docs/references/module-decomposition.md +38 -0
  100. package/skills/docs/references/profiles.md +60 -0
  101. package/skills/docs/references/quality-attributes.md +27 -0
  102. package/skills/docs/references/reverse-engineer.md +21 -0
  103. package/skills/docs/references/spec-depths.md +31 -0
  104. package/skills/docs/references/summarize.md +18 -0
  105. package/skills/docs/references/surfaces-and-actions.md +24 -0
  106. package/skills/docs/references/validation.md +47 -0
  107. package/skills/docs/references/workflows-and-jobs.md +25 -0
  108. package/skills/docs/scripts/ingest.mjs +984 -0
  109. package/skills/docs/scripts/profile-detect.mjs +299 -0
  110. package/skills/docs/scripts/screen.mjs +96 -0
  111. package/skills/docs/scripts/template-lint.mjs +143 -0
  112. package/skills/docs/scripts/validate-docs.mjs +363 -0
  113. package/skills/doctor/SKILL.md +167 -0
  114. package/skills/feature/SKILL.md +132 -0
  115. package/skills/init/SKILL.md +137 -0
  116. package/skills/project/SKILL.md +261 -0
  117. package/skills/project/references/commands.md +213 -0
  118. package/skills/resume/SKILL.md +79 -0
  119. package/skills/review/SKILL.md +91 -0
  120. package/skills/status/SKILL.md +85 -0
  121. package/skills/task/SKILL.md +183 -0
  122. package/src/cli/product-map.mjs +468 -0
  123. package/src/cli/render.mjs +544 -0
  124. package/src/cli.mjs +2774 -0
  125. package/src/cloud/crypto.mjs +118 -0
  126. package/src/cloud/merge.mjs +186 -0
  127. package/src/cloud/policy.mjs +115 -0
  128. package/src/cloud/sync.mjs +512 -0
  129. package/src/cloud/transport.mjs +116 -0
  130. package/src/db/connect.mjs +189 -0
  131. package/src/db/maintenance.mjs +419 -0
  132. package/src/db/migrate.mjs +188 -0
  133. package/src/db/migrations/001_initial.sql +1024 -0
  134. package/src/db/migrations/002_docs_coverage.sql +126 -0
  135. package/src/db/migrations/003_memory_retrieval_and_integrity.sql +168 -0
  136. package/src/db/migrations/004_task_categories.sql +59 -0
  137. package/src/db/migrations/005_executable_evidence.sql +32 -0
  138. package/src/db/migrations/006_module_and_feature_boundaries.sql +27 -0
  139. package/src/db/migrations/007_drop_redundant_module_users.sql +13 -0
  140. package/src/db/migrations/008_changes_assumptions_test_plans_api_services.sql +166 -0
  141. package/src/db/migrations/009_retired_documents.sql +54 -0
  142. package/src/db/migrations/010_test_plan_evidence.sql +20 -0
  143. package/src/db/migrations/011_criterion_waiver.sql +13 -0
  144. package/src/db/migrations/012_synchronization.sql +56 -0
  145. package/src/db/store.mjs +327 -0
  146. package/src/decisions/record.mjs +167 -0
  147. package/src/docs/proposals.mjs +871 -0
  148. package/src/docs/render.mjs +1424 -0
  149. package/src/docs/templates.mjs +1281 -0
  150. package/src/features/acceptance.mjs +315 -0
  151. package/src/features/specify.mjs +248 -0
  152. package/src/init/discovery.mjs +901 -0
  153. package/src/init/index.mjs +784 -0
  154. package/src/init/questions.mjs +562 -0
  155. package/src/memory/benchmark.mjs +171 -0
  156. package/src/memory/capture.mjs +135 -0
  157. package/src/memory/consolidate.mjs +255 -0
  158. package/src/memory/index.mjs +810 -0
  159. package/src/model/ids.mjs +185 -0
  160. package/src/model/screening.mjs +150 -0
  161. package/src/model/toolkit.mjs +258 -0
  162. package/src/model/vocabulary.mjs +190 -0
  163. package/src/product/assumptions.mjs +120 -0
  164. package/src/product/changes.mjs +180 -0
  165. package/src/product/test-plans.mjs +183 -0
  166. package/src/progress/index.mjs +1364 -0
  167. package/src/runtime/harness.mjs +264 -0
  168. package/src/runtime/hooks.mjs +1021 -0
  169. package/src/runtime/identity.mjs +305 -0
  170. package/src/runtime/resume.mjs +343 -0
  171. package/src/runtime/session.mjs +679 -0
  172. package/src/runtime/version.mjs +315 -0
  173. package/src/service/assets/control-center.html +206 -0
  174. package/src/service/assets/control-center.manifest.json +7 -0
  175. package/src/service/manage.mjs +542 -0
  176. package/src/service/mutations.mjs +860 -0
  177. package/src/service/read-model.mjs +1557 -0
  178. package/src/service/server.mjs +783 -0
  179. package/src/tasks/categories.mjs +154 -0
  180. package/src/tasks/derive.mjs +977 -0
  181. package/src/tasks/lifecycle.mjs +830 -0
  182. package/src/verify/index.mjs +236 -0
@@ -0,0 +1,171 @@
1
+ // Measure what memory retrieval actually does.
2
+ //
3
+ // Section 15.12 lists eleven things that must be measured before Claude Mem can
4
+ // be dropped as a transitional provider: recall, precision, noise, ranking
5
+ // quality, token reduction, latency, storage growth, stale detection,
6
+ // contradiction detection, cross session resume accuracy and cross agent
7
+ // handoff accuracy. None of it was measured, so DEC-TBD-002 could not be
8
+ // answered even in principle: there was no number to hold a threshold against.
9
+ //
10
+ // This measures. It does not decide. What counts as good enough is the owner's
11
+ // call and section 23 leaves it open; what the numbers are is not a matter of
12
+ // opinion and is answered here.
13
+ //
14
+ // The queries are built from the memories themselves rather than written by
15
+ // hand, so the benchmark grows with the project instead of testing a fixture
16
+ // that stops resembling it. A query is a term drawn from one memory's title,
17
+ // and the memory it came from is the answer it should find.
18
+
19
+ import { query } from "../db/store.mjs";
20
+ import { recall, tokenize } from "./index.mjs";
21
+
22
+ /** Words too common to tell one memory from another. */
23
+ const WEAK = new Set([
24
+ "task", "complete", "completed", "recorded", "moved", "the", "and", "for",
25
+ "with", "that", "this", "from", "superdev", "project", "feature", "decision",
26
+ ]);
27
+
28
+ /**
29
+ * Build the questions from the memories.
30
+ *
31
+ * Each query is the rarest word in a memory's title, and the memory it came
32
+ * from is the one answer that query should return. Picking the rarest word is
33
+ * deliberate: a common word would match everything and measure nothing, and the
34
+ * point is to find out whether retrieval can single out one entry.
35
+ */
36
+ async function buildQueries(root, limit) {
37
+ const entries = await query(root, (db) => db.all(
38
+ `SELECT id, title, content, kind FROM memory_entries
39
+ WHERE superseded_by IS NULL ORDER BY id`));
40
+ if (entries.length < 3) return { entries, queries: [] };
41
+
42
+ const frequency = new Map();
43
+ for (const e of entries) {
44
+ for (const term of new Set(tokenize(`${e.title} ${e.content}`))) {
45
+ frequency.set(term, (frequency.get(term) ?? 0) + 1);
46
+ }
47
+ }
48
+
49
+ const queries = [];
50
+ for (const e of entries) {
51
+ const terms = [...new Set(tokenize(e.title))]
52
+ .filter((t) => t.length > 3 && !WEAK.has(t))
53
+ .sort((a, b) => (frequency.get(a) ?? 0) - (frequency.get(b) ?? 0));
54
+ if (!terms.length) continue;
55
+ queries.push({ text: terms.slice(0, 2).join(" "), expect: e.id });
56
+ if (queries.length >= limit) break;
57
+ }
58
+ return { entries, queries };
59
+ }
60
+
61
+ /**
62
+ * Run the benchmark.
63
+ *
64
+ * Every number here is observed. Where something cannot be measured from a
65
+ * single run, it says so rather than producing a figure that looks like a
66
+ * measurement and is not.
67
+ */
68
+ export async function benchmark(root, { limit = 40 } = {}) {
69
+ const { entries, queries } = await buildQueries(root, limit);
70
+ if (queries.length === 0) {
71
+ return {
72
+ measurable: false,
73
+ why: entries.length < 3
74
+ ? `Only ${entries.length} memories are stored. Retrieval quality cannot be measured until there is something to retrieve from.`
75
+ : "No memory title carries a word distinctive enough to query on, so no question can be asked that has one right answer.",
76
+ entries: entries.length,
77
+ };
78
+ }
79
+
80
+ let hits = 0;
81
+ let reciprocalRankTotal = 0;
82
+ let returnedTotal = 0;
83
+ let bytesReturned = 0;
84
+ const latencies = [];
85
+
86
+ for (const q of queries) {
87
+ const started = process.hrtime.bigint();
88
+ const found = await recall(root, { text: q.text, limit: 5 }).catch(() => []);
89
+ latencies.push(Number(process.hrtime.bigint() - started) / 1e6);
90
+
91
+ const rows = Array.isArray(found) ? found : (found?.entries ?? []);
92
+ returnedTotal += rows.length;
93
+ bytesReturned += rows.reduce((n, r) => n + String(r.content ?? "").length, 0);
94
+
95
+ const rank = rows.findIndex((r) => r.id === q.expect);
96
+ if (rank >= 0) {
97
+ hits += 1;
98
+ // Mean reciprocal rank: finding the right answer first is worth more than
99
+ // finding it fifth, which a plain hit rate cannot express.
100
+ reciprocalRankTotal += 1 / (rank + 1);
101
+ }
102
+ }
103
+
104
+ const wholeCorpusBytes = entries.reduce((n, e) => n + String(e.content ?? "").length, 0);
105
+ const storage = await query(root, async (db) => ({
106
+ terms: Number(Object.values(await db.get("SELECT COUNT(*) AS c FROM memory_search_terms"))[0]),
107
+ links: Number(Object.values(await db.get("SELECT COUNT(*) AS c FROM memory_links"))[0]),
108
+ superseded: Number(Object.values(await db.get(
109
+ "SELECT COUNT(*) AS c FROM memory_entries WHERE superseded_by IS NOT NULL"))[0]),
110
+ contradicted: Number(Object.values(await db.get(
111
+ "SELECT COUNT(*) AS c FROM memory_entries WHERE epistemic_status = 'contradicted'"))[0]),
112
+ }));
113
+
114
+ const sorted = [...latencies].sort((a, b) => a - b);
115
+ const round = (n) => Math.round(n * 100) / 100;
116
+
117
+ return {
118
+ measurable: true,
119
+ corpus: { entries: entries.length, queries: queries.length },
120
+ // Section 15.12's list, each answered by something observed.
121
+ recall: {
122
+ value: round(hits / queries.length),
123
+ says: `${hits} of ${queries.length} questions returned the memory they were drawn from within the top five.`,
124
+ },
125
+ precision: {
126
+ value: round(hits / Math.max(1, returnedTotal)),
127
+ says: `${returnedTotal} entries were returned across ${queries.length} questions to surface ${hits} right answers.`,
128
+ },
129
+ noise: {
130
+ value: round((returnedTotal - hits) / Math.max(1, returnedTotal)),
131
+ says: "The share of returned entries that were not the one asked for. Some of those are genuinely related, so this is an upper bound on noise rather than a count of wrong answers.",
132
+ },
133
+ ranking: {
134
+ value: round(reciprocalRankTotal / queries.length),
135
+ says: "Mean reciprocal rank. One means the right answer always came first; a half means it typically came second.",
136
+ },
137
+ tokenReduction: {
138
+ value: round(1 - bytesReturned / Math.max(1, wholeCorpusBytes * queries.length)),
139
+ says: `Retrieval returned ${bytesReturned} characters where handing over the whole corpus for every question would have been ${wholeCorpusBytes * queries.length}.`,
140
+ },
141
+ latencyMs: {
142
+ median: round(sorted[Math.floor(sorted.length / 2)] ?? 0),
143
+ worst: round(sorted.at(-1) ?? 0),
144
+ says: "Wall clock per question, including opening and closing the database, because that is what a caller waits for.",
145
+ },
146
+ storageGrowth: {
147
+ entries: entries.length,
148
+ searchTerms: storage.terms,
149
+ links: storage.links,
150
+ says: `${round(storage.terms / Math.max(1, entries.length))} index terms per memory. Growth is linear in entries, and the index is rebuilt rather than accumulated.`,
151
+ },
152
+ staleDetection: {
153
+ superseded: storage.superseded,
154
+ says: "Superseding is recorded rather than deleted, so a memory that stopped being true stays readable with what replaced it.",
155
+ },
156
+ contradictionDetection: {
157
+ marked: storage.contradicted,
158
+ says: "Consolidation marks the earlier of two contradicting statements and keeps both, so recall can warn rather than pick a side.",
159
+ },
160
+ // Both of these are journeys rather than queries, which section 20.1 asks
161
+ // for directly. Saying so beats inventing a number.
162
+ resumeAccuracy: {
163
+ measurable: false,
164
+ says: "Not a query. Run superdev resume in a fresh process and check whether the working state comes back, which is the only honest measurement of it.",
165
+ },
166
+ handoffAccuracy: {
167
+ measurable: false,
168
+ says: "Not a query. Hand a session to another agent and check what it can answer without the conversation.",
169
+ },
170
+ };
171
+ }
@@ -0,0 +1,135 @@
1
+ // Capture memory from the events worth remembering.
2
+ //
3
+ // Section 15.6 lists when capture should happen: task start, task completion,
4
+ // decision acceptance and supersession, blocker creation and resolution, a
5
+ // failed approach, successful verification, session handoff, documentation
6
+ // acceptance, material product change. remember() existed and nothing called
7
+ // it, so after a night of work the memory system held nothing and section 12.8's
8
+ // memory show and memory verify had nothing to show or verify.
9
+ //
10
+ // What is captured is deliberately narrow. Section 15.2 says memory must avoid
11
+ // storing an agent's own unverified output as fact, so an outcome is only
12
+ // recorded when evidence backs it, and its epistemic status says which: a
13
+ // completion with passing evidence is confirmed, everything else is inferred.
14
+ //
15
+ // Capture is idempotent because section 15.6 requires it. Every entry carries a
16
+ // dedupe key built from the event it came from, so a hook that fires twice, a
17
+ // retried command and a replayed session all produce one memory.
18
+
19
+ import { remember, linkMemory } from "./index.mjs";
20
+ import { query } from "../db/store.mjs";
21
+
22
+ /** What each lifecycle moment becomes, in the vocabulary the schema allows. */
23
+ const KIND = {
24
+ task_completed: "outcome",
25
+ task_blocked: "blocker",
26
+ task_unblocked: "outcome",
27
+ decision_recorded: "decision",
28
+ decision_superseded: "decision",
29
+ verification_passed: "outcome",
30
+ verification_failed: "learned_fact",
31
+ question_answered: "learned_fact",
32
+ change_recorded: "learned_fact",
33
+ assumption_recorded: "unresolved_question",
34
+ };
35
+
36
+ /**
37
+ * Record one moment, once.
38
+ *
39
+ * Failure never propagates. Memory is recall, not authority, and losing a
40
+ * recall entry must not fail the work that produced it: a task that completed
41
+ * has completed whether or not anything remembered it.
42
+ */
43
+ export async function capture(root, event = {}) {
44
+ try {
45
+ const kind = KIND[event.type];
46
+ if (!kind) return null;
47
+ const title = String(event.title ?? "").trim();
48
+ const content = String(event.content ?? "").trim();
49
+ if (!title || !content) return null;
50
+
51
+ const entry = await remember(root, {
52
+ kind,
53
+ title: title.slice(0, 200),
54
+ content: content.slice(0, 2000),
55
+ // Confirmed only when something was observed. An agent saying a thing
56
+ // happened is inferred, and section 15.2 forbids treating that as fact.
57
+ epistemicStatus: event.confirmed ? "confirmed" : "inferred",
58
+ sourceRef: event.sourceRef ?? null,
59
+ // The event identity, not the wording. Two different observations can be
60
+ // worded alike and collapsing those would lose real information.
61
+ dedupeKey: event.dedupeKey ?? `${event.type}:${event.subjectId ?? title}`,
62
+ taskId: event.taskId ?? null,
63
+ featureId: event.featureId ?? null,
64
+ sessionId: event.sessionId ?? null,
65
+ });
66
+
67
+ // Section 15.4 says long-term memory must use stable links to the records
68
+ // it concerns, and 14.2 says a memory link cannot reference a missing one.
69
+ for (const link of event.links ?? []) {
70
+ if (!link?.type || !link?.id) continue;
71
+ await linkMemory(root, entry.id, link.type, link.id, link.relationship ?? "concerns").catch(() => {});
72
+ }
73
+ return entry;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Sweep the activity trail for moments that were never remembered.
81
+ *
82
+ * Capture is wired into the lifecycle from here on, but a project that ran
83
+ * before it existed has a history full of moments and no memory of any of them.
84
+ * This reads that history and records what it finds, which is also the recovery
85
+ * path if capture is ever switched off and back on.
86
+ *
87
+ * Only events that carry a claim are taken. An activity trail holds every file
88
+ * touch and every regeneration, and remembering those would bury the handful of
89
+ * things worth recalling.
90
+ */
91
+ export async function captureFromHistory(root, { limit = 500, apply = false } = {}) {
92
+ const events = await query(root, (db) => db.all(
93
+ `SELECT e.*, t.name AS task_name, t.status AS task_status
94
+ FROM activity_events e
95
+ LEFT JOIN tasks t ON t.id = e.task_id
96
+ WHERE e.event_type IN ('task_completed', 'task_blocked', 'decision_recorded',
97
+ 'verification_attached', 'question_answered', 'scope_changed')
98
+ ORDER BY e.sequence DESC LIMIT ?`, limit));
99
+
100
+ const worth = [];
101
+ for (const e of events) {
102
+ const summary = String(e.summary ?? "");
103
+ if (e.event_type === "verification_attached" && !/^Passing/.test(summary)) continue;
104
+ // A scope change is only worth remembering when it says what moved, and
105
+ // most of them are routine record writes during setup.
106
+ if (e.event_type === "scope_changed" && !/decision|superseded|cancelled|removed/i.test(summary)) continue;
107
+ worth.push(e);
108
+ }
109
+
110
+ const plan = { found: events.length, worthRemembering: worth.length, recorded: 0 };
111
+ if (!apply) return plan;
112
+
113
+ for (const e of worth) {
114
+ const type = e.event_type === "verification_attached" ? "verification_passed" : e.event_type;
115
+ const made = await capture(root, {
116
+ type,
117
+ title: String(e.summary ?? "").slice(0, 120),
118
+ content: String(e.summary ?? ""),
119
+ // Read from the trail rather than asserted now: the event is the record
120
+ // that it happened, which is what makes this confirmed rather than
121
+ // inferred.
122
+ confirmed: type === "verification_passed" || type === "task_completed",
123
+ sourceRef: e.id,
124
+ dedupeKey: `event:${e.id}`,
125
+ taskId: e.task_id ?? null,
126
+ featureId: e.feature_id ?? null,
127
+ links: [
128
+ e.task_id ? { type: "task", id: e.task_id } : null,
129
+ e.feature_id ? { type: "feature", id: e.feature_id } : null,
130
+ ].filter(Boolean),
131
+ });
132
+ if (made) plan.recorded += 1;
133
+ }
134
+ return plan;
135
+ }
@@ -0,0 +1,255 @@
1
+ // Keeping memory worth reading.
2
+ //
3
+ // Section 15.9 asks the memory system to periodically merge duplicates, connect
4
+ // related memories, supersede outdated statements, discard low value noise,
5
+ // detect contradictions, preserve provenance and recalculate retrieval
6
+ // metadata. Without that, memory grows until recall returns everything and
7
+ // therefore nothing.
8
+ //
9
+ // Nothing here deletes a memory that carries a claim. Superseding keeps the
10
+ // statement and marks what replaced it, because a memory that was believed and
11
+ // turned out wrong is itself worth knowing. Only memories that carry no claim,
12
+ // the operational noise section 15.9 names, are removed outright.
13
+ //
14
+ // Every judgement is structural rather than semantic: identical content, an
15
+ // exact contradiction of an earlier statement, a link to a record that no
16
+ // longer exists. A consolidation pass that guessed at meaning would quietly
17
+ // rewrite what the project believes.
18
+
19
+ import { mutate, query } from "../db/store.mjs";
20
+ import { tokenize } from "./index.mjs";
21
+
22
+ /** Memory kinds that carry a claim worth keeping even once superseded. */
23
+ const CLAIM_KINDS = new Set(["decision", "learned_fact", "outcome", "blocker", "unresolved_question"]);
24
+
25
+ /** How stale a session-scoped summary has to be before it is noise. */
26
+ const NOISE_AFTER_DAYS = 30;
27
+
28
+ const DAY = 86_400_000;
29
+
30
+ /**
31
+ * Run a consolidation pass.
32
+ *
33
+ * Without `apply` nothing changes, which makes this safe to run at any time and
34
+ * is the mode worth putting in front of a person: it says what would be merged
35
+ * and what contradicts what, without changing the answer.
36
+ */
37
+ export async function consolidate(root, { apply = false, now = Date.now() } = {}) {
38
+ const state = await query(root, async (db) => ({
39
+ entries: await db.all(
40
+ `SELECT id, project_id, kind, title, content, content_hash, epistemic_status,
41
+ superseded_by, created_at, session_id, task_id, feature_id
42
+ FROM memory_entries ORDER BY created_at, id`),
43
+ links: await db.all("SELECT memory_id, target_type, target_id FROM memory_links"),
44
+ terms: await db.all("SELECT memory_id, COUNT(*) n FROM memory_search_terms GROUP BY memory_id"),
45
+ }));
46
+
47
+ const live = state.entries.filter((e) => !e.superseded_by);
48
+ const termCount = new Map(state.terms.map((t) => [t.memory_id, t.n]));
49
+
50
+ // Duplicates: the same content said twice. The earliest is kept because it is
51
+ // the one other records may already point at, and the later ones are marked
52
+ // as superseded by it rather than deleted.
53
+ const byHash = new Map();
54
+ const duplicates = [];
55
+ for (const entry of live) {
56
+ const key = entry.content_hash || `${entry.kind}:${String(entry.content ?? "").trim()}`;
57
+ if (!key) continue;
58
+ if (byHash.has(key)) duplicates.push({ duplicate: entry, keep: byHash.get(key) });
59
+ else byHash.set(key, entry);
60
+ }
61
+
62
+ // Contradictions: two live memories about the same records saying opposite
63
+ // things. Detected structurally, by one statement negating another that
64
+ // shares its subject, never by judging which is true.
65
+ const linksByMemory = new Map();
66
+ for (const l of state.links) {
67
+ if (!linksByMemory.has(l.memory_id)) linksByMemory.set(l.memory_id, new Set());
68
+ linksByMemory.get(l.memory_id).add(`${l.target_type}:${l.target_id}`);
69
+ }
70
+ const contradictions = [];
71
+ for (let i = 0; i < live.length; i += 1) {
72
+ for (let j = i + 1; j < live.length; j += 1) {
73
+ const a = live[i];
74
+ const b = live[j];
75
+ const sharedTargets = [...(linksByMemory.get(a.id) ?? [])]
76
+ .filter((t) => (linksByMemory.get(b.id) ?? new Set()).has(t));
77
+ if (sharedTargets.length === 0) continue;
78
+ if (!negates(a.content, b.content)) continue;
79
+ contradictions.push({ earlier: a.id, later: b.id, about: sharedTargets[0] });
80
+ }
81
+ }
82
+
83
+ // Noise: a session summary old enough that its session is long over, carrying
84
+ // no claim and linked to nothing. Section 15.9 asks for these to be
85
+ // discarded, and they are the only thing here that is.
86
+ const linked = new Set(state.links.map((l) => l.memory_id));
87
+ const noise = live.filter((e) =>
88
+ !CLAIM_KINDS.has(e.kind)
89
+ && !linked.has(e.id)
90
+ && now - Date.parse(e.created_at || 0) > NOISE_AFTER_DAYS * DAY);
91
+
92
+ // Retrieval metadata that was never built. A memory with no search terms
93
+ // cannot be found lexically, which is the first retrieval stage 15.7 names.
94
+ const missingTerms = live.filter((e) => !termCount.get(e.id));
95
+
96
+ // Links pointing at records that no longer exist. Section 14.2 says memory
97
+ // links cannot reference missing records.
98
+ const danglingLinks = await query(root, async (db) => {
99
+ const out = [];
100
+ const tables = {
101
+ goal: "goals", milestone: "milestones", module: "modules", feature: "features",
102
+ workflow: "workflows", task: "tasks", decision: "decisions", data_entity: "data_entities",
103
+ surface: "surfaces", api_operation: "api_operations", integration: "integrations",
104
+ change: "changes", assumption: "assumptions", question: "questions",
105
+ };
106
+ for (const l of state.links) {
107
+ const table = tables[l.target_type];
108
+ if (!table) continue;
109
+ const hit = await db.get(`SELECT 1 AS ok FROM ${table} WHERE id = ?`, l.target_id);
110
+ if (!hit) out.push(l);
111
+ }
112
+ return out;
113
+ });
114
+
115
+ const plan = {
116
+ applied: apply,
117
+ live: live.length,
118
+ duplicatesMerged: duplicates.length,
119
+ contradictionsFound: contradictions.length,
120
+ noiseDiscarded: noise.length,
121
+ retrievalTermsRebuilt: missingTerms.length,
122
+ danglingLinksRemoved: danglingLinks.length,
123
+ contradictions: contradictions.slice(0, 10),
124
+ };
125
+
126
+ if (!apply) return plan;
127
+
128
+ await mutate(root, async (db) => {
129
+ const at = new Date().toISOString();
130
+
131
+ for (const { duplicate, keep } of duplicates) {
132
+ await db.run("UPDATE memory_entries SET superseded_by = ? WHERE id = ?", keep.id, duplicate.id);
133
+ }
134
+
135
+ // A contradiction is not resolved here. Both statements stay, and the
136
+ // earlier one is marked contradicted so recall can warn rather than pick a
137
+ // side, which is what 15.8 asks of a recalled memory.
138
+ for (const c of contradictions) {
139
+ await db.run("UPDATE memory_entries SET epistemic_status = ? WHERE id = ?", "contradicted", c.earlier);
140
+ }
141
+
142
+ for (const entry of noise) {
143
+ await db.run("DELETE FROM memory_search_terms WHERE memory_id = ?", entry.id);
144
+ await db.run("DELETE FROM memory_entries WHERE id = ?", entry.id);
145
+ }
146
+
147
+ for (const entry of missingTerms) {
148
+ if (noise.some((n) => n.id === entry.id)) continue;
149
+ await indexTerms(db, entry);
150
+ }
151
+
152
+ for (const l of danglingLinks) {
153
+ await db.run(
154
+ "DELETE FROM memory_links WHERE memory_id = ? AND target_type = ? AND target_id = ?",
155
+ l.memory_id, l.target_type, l.target_id);
156
+ }
157
+
158
+ const project = await db.get("SELECT id FROM projects LIMIT 1");
159
+ if (project) {
160
+ await db.run(
161
+ `INSERT INTO activity_events (id, project_id, actor_label, event_type, summary, created_at, sequence)
162
+ VALUES ((SELECT 'EVT-' || printf('%04d', COALESCE(MAX(CAST(SUBSTR(id, 5) AS INTEGER)), 0) + 1) FROM activity_events),
163
+ ?, ?, ?, ?, ?,
164
+ (SELECT COALESCE(MAX(sequence), 0) + 1 FROM activity_events))`,
165
+ project.id, "superdev", "memory_consolidated",
166
+ `Memory consolidated: ${duplicates.length} duplicates merged, ${contradictions.length} contradictions marked, ${noise.length} discarded.`,
167
+ at,
168
+ );
169
+ }
170
+ });
171
+
172
+ return plan;
173
+ }
174
+
175
+ /** Rebuild the lexical index for one entry, which is retrieval stage three. */
176
+ async function indexTerms(db, entry) {
177
+ const fields = [["title", entry.title, 3], ["content", entry.content, 1]];
178
+ const counts = new Map();
179
+ for (const [field, text, weight] of fields) {
180
+ for (const term of tokenize(text ?? "")) {
181
+ const key = `${field}:${term}`;
182
+ counts.set(key, { field, term, weight, n: (counts.get(key)?.n ?? 0) + 1 });
183
+ }
184
+ }
185
+ for (const { field, term, weight, n } of counts.values()) {
186
+ await db.run(
187
+ `INSERT OR REPLACE INTO memory_search_terms (memory_id, term, field, frequency, field_weight)
188
+ VALUES (?, ?, ?, ?, ?)`,
189
+ entry.id, term, field, n, weight,
190
+ );
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Does one statement negate another?
196
+ *
197
+ * Structural only: the same words with a negation on one side and not the
198
+ * other. This finds "the service holds the lock" against "the service does not
199
+ * hold the lock" and finds nothing clever, which is the intent. Judging meaning
200
+ * would let a consolidation pass rewrite what the project believes.
201
+ */
202
+ function negates(a, b) {
203
+ const NEG = /\b(not|never|no longer|cannot|does not|is not|was not)\b/i;
204
+ const strip = (t) => String(t ?? "").toLowerCase().replace(NEG, " ").replace(/\s+/g, " ").trim();
205
+ const aNeg = NEG.test(String(a ?? ""));
206
+ const bNeg = NEG.test(String(b ?? ""));
207
+ if (aNeg === bNeg) return false;
208
+ const sa = strip(a);
209
+ const sb = strip(b);
210
+ if (!sa || !sb) return false;
211
+ // The remaining words have to be substantially the same statement, not merely
212
+ // on the same topic.
213
+ const wa = new Set(sa.split(" ").filter((w) => w.length > 3));
214
+ const wb = new Set(sb.split(" ").filter((w) => w.length > 3));
215
+ if (wa.size < 3 || wb.size < 3) return false;
216
+ const shared = [...wa].filter((w) => wb.has(w)).length;
217
+ return shared / Math.max(wa.size, wb.size) > 0.7;
218
+ }
219
+
220
+ /**
221
+ * What the memory system currently holds, and what it cannot do.
222
+ *
223
+ * Section 15.11 requires saying plainly that semantic retrieval is a bounded
224
+ * scan when the engine has no vector index, and never describing it as indexed
225
+ * vector search. That sentence is here rather than in a comment because it is
226
+ * the answer to a question a person asks this command.
227
+ */
228
+ export async function memoryStatus(root) {
229
+ const s = await query(root, async (db) => ({
230
+ total: (await db.get("SELECT COUNT(*) n FROM memory_entries")).n,
231
+ live: (await db.get("SELECT COUNT(*) n FROM memory_entries WHERE superseded_by IS NULL")).n,
232
+ superseded: (await db.get("SELECT COUNT(*) n FROM memory_entries WHERE superseded_by IS NOT NULL")).n,
233
+ byKind: await db.all("SELECT kind, COUNT(*) n FROM memory_entries WHERE superseded_by IS NULL GROUP BY kind ORDER BY n DESC"),
234
+ byStatus: await db.all("SELECT epistemic_status, COUNT(*) n FROM memory_entries WHERE superseded_by IS NULL GROUP BY epistemic_status ORDER BY n DESC"),
235
+ links: (await db.get("SELECT COUNT(*) n FROM memory_links")).n,
236
+ terms: (await db.get("SELECT COUNT(*) n FROM memory_search_terms")).n,
237
+ indexed: (await db.get("SELECT COUNT(DISTINCT memory_id) n FROM memory_search_terms")).n,
238
+ embeddings: (await db.get("SELECT COUNT(*) n FROM memory_embeddings")).n,
239
+ oldest: (await db.get("SELECT MIN(created_at) t FROM memory_entries")).t,
240
+ newest: (await db.get("SELECT MAX(created_at) t FROM memory_entries")).t,
241
+ }));
242
+
243
+ return {
244
+ ...s,
245
+ notIndexedForSearch: Math.max(0, s.live - s.indexed),
246
+ retrieval: {
247
+ // Section 15.7's stages, and which of them this actually performs.
248
+ stages: ["structured filters", "record relationships", "lexical retrieval", "recency",
249
+ "memory type weighting", "epistemic status weighting"],
250
+ semantic: s.embeddings > 0
251
+ ? "A bounded scan over stored vectors. The local engine has no vector index, so this is not indexed vector search."
252
+ : "Not in use. No embeddings are stored, and section 15.11 leaves the provider undecided.",
253
+ },
254
+ };
255
+ }