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,118 @@
1
+ // Encryption for anything that leaves the machine. DEC-TBD-008.
2
+ //
3
+ // The key is generated here, stored here, and never transmitted. What a remote
4
+ // holds is unreadable without it, which is the whole point: a directory on a
5
+ // shared drive, and later a hosted service, both get bytes that say nothing.
6
+ //
7
+ // Losing the key costs the remote copy and nothing else. That is only an
8
+ // acceptable trade because DEC-0014 makes the local database authoritative, so
9
+ // recovery is the local backup rather than the remote. A product whose only
10
+ // copy was remote could not afford an owner-held key, and this one can.
11
+
12
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
13
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+
16
+ import { paths } from "../db/store.mjs";
17
+
18
+ export const E = {
19
+ NO_KEY: "E_NO_SYNC_KEY",
20
+ WRONG_KEY: "E_WRONG_SYNC_KEY",
21
+ CORRUPT: "E_BUNDLE_CORRUPT",
22
+ };
23
+
24
+ export class CryptoError extends Error {
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.name = "CryptoError";
28
+ this.code = code;
29
+ }
30
+ }
31
+
32
+ const keyFile = (root) => join(paths(root).dir, "cloud", "key");
33
+
34
+ /**
35
+ * The key for this project, created on first use.
36
+ *
37
+ * Written with owner-only permissions, in the project's own directory rather
38
+ * than a shared one, because a key that lives next to the thing it protects is
39
+ * a key somebody can find when they need it and can delete when they mean to.
40
+ */
41
+ export function ensureKey(root) {
42
+ const file = keyFile(root);
43
+ if (existsSync(file)) return readFileSync(file);
44
+ const key = randomBytes(32);
45
+ mkdirSync(dirname(file), { recursive: true });
46
+ writeFileSync(file, key);
47
+ try {
48
+ chmodSync(file, 0o600);
49
+ } catch {
50
+ // A filesystem without permissions is not a reason to refuse the key.
51
+ }
52
+ return key;
53
+ }
54
+
55
+ export function keyExists(root) {
56
+ return existsSync(keyFile(root));
57
+ }
58
+
59
+ export function loadKey(root) {
60
+ const file = keyFile(root);
61
+ if (!existsSync(file)) {
62
+ throw new CryptoError(E.NO_KEY,
63
+ "No encryption key exists for this project, so nothing can be read from or written to a remote. Run superdev cloud connect to create one.");
64
+ }
65
+ return readFileSync(file);
66
+ }
67
+
68
+ /**
69
+ * A short, stable name for a key.
70
+ *
71
+ * Two machines can compare fingerprints to find out whether they hold the same
72
+ * key without either sending it. A mismatch is the difference between "the
73
+ * bundle is corrupt" and "you have the wrong key", and those need different
74
+ * answers from whoever is reading the error.
75
+ */
76
+ export const fingerprint = (key) =>
77
+ createHash("sha256").update(key).digest("hex").slice(0, 16);
78
+
79
+ /** Encrypt a JSON-serializable value. Returns bytes, not text. */
80
+ export function seal(key, value) {
81
+ const iv = randomBytes(12);
82
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
83
+ const body = Buffer.concat([
84
+ cipher.update(Buffer.from(JSON.stringify(value), "utf8")),
85
+ cipher.final(),
86
+ ]);
87
+ // Version byte first, so a future format change is detectable rather than
88
+ // arriving as a decryption failure nobody can explain.
89
+ return Buffer.concat([Buffer.from([1]), iv, cipher.getAuthTag(), body]);
90
+ }
91
+
92
+ /** Decrypt what seal produced, or say plainly which of the two things went wrong. */
93
+ export function open(key, bytes) {
94
+ const buffer = Buffer.from(bytes);
95
+ if (buffer.length < 30 || buffer[0] !== 1) {
96
+ throw new CryptoError(E.CORRUPT,
97
+ "This does not look like a Superdev bundle. It is either truncated or written by a different version.");
98
+ }
99
+ const iv = buffer.subarray(1, 13);
100
+ const tag = buffer.subarray(13, 29);
101
+ const body = buffer.subarray(29);
102
+ try {
103
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
104
+ decipher.setAuthTag(tag);
105
+ const plain = Buffer.concat([decipher.update(body), decipher.final()]);
106
+ return JSON.parse(plain.toString("utf8"));
107
+ } catch {
108
+ throw new CryptoError(E.WRONG_KEY,
109
+ "The bundle could not be opened with this project's key. Either it was written under a different key, or it has been altered since it was written. Nothing has been read from it.");
110
+ }
111
+ }
112
+
113
+ /** A stable hash of a record, for deciding whether it changed. */
114
+ export const rowHash = (row) =>
115
+ createHash("sha256")
116
+ .update(JSON.stringify(Object.keys(row).sort().map((k) => [k, row[k]])))
117
+ .digest("hex")
118
+ .slice(0, 32);
@@ -0,0 +1,186 @@
1
+ // The merge, the conflict, and the lease. DEC-TBD-006.
2
+ //
3
+ // Local first means the local database is always the authority: nothing here
4
+ // ever overwrites a local record without either a clear reason or an explicit
5
+ // resolution. A remote change to a record nobody touched locally is applied. A
6
+ // remote change to a record that also changed locally is refused and recorded
7
+ // as a conflict, because the alternative, last writer wins, loses somebody's
8
+ // work silently. Silence is the failure this product exists to prevent.
9
+ //
10
+ // Three-way, not two-way. Comparing local against remote can only say they
11
+ // differ; it cannot say who moved. The base, what both sides agreed at the end
12
+ // of the last successful sync, is what turns a difference into a fact: changed
13
+ // there, changed here, or changed in both places.
14
+ //
15
+ // Append only history is never merged. Activity does not cross the boundary at
16
+ // all under DEC-TBD-007, and nothing here rewrites a sequence.
17
+
18
+ import { rowHash } from "./crypto.mjs";
19
+ import { isShared, project } from "./policy.mjs";
20
+
21
+ /** What a comparison can conclude. */
22
+ export const OUTCOME = {
23
+ UNCHANGED: "unchanged",
24
+ REMOTE_ONLY: "remote_only",
25
+ LOCAL_ONLY: "local_only",
26
+ BOTH: "both",
27
+ NEW_REMOTE: "new_remote",
28
+ NEW_LOCAL: "new_local",
29
+ };
30
+
31
+ /**
32
+ * Compare one record across the three points.
33
+ *
34
+ * `base` is what both sides last agreed, and its absence means this record has
35
+ * never synced: it is new to one side or the other rather than conflicted. A
36
+ * record present on both sides with no base and the same content is treated as
37
+ * unchanged, which happens whenever two copies come from the same export.
38
+ */
39
+ export function compare({ local, remote, base }) {
40
+ const localHash = local ? rowHash(local) : null;
41
+ const remoteHash = remote ? rowHash(remote) : null;
42
+
43
+ if (!local && !remote) return { outcome: OUTCOME.UNCHANGED };
44
+ if (!local) return { outcome: OUTCOME.NEW_REMOTE, hash: remoteHash };
45
+ if (!remote) {
46
+ // No base means this side made it and the other has not seen it. A base
47
+ // means the other side deleted it, and deletion does not cross: a record
48
+ // removed elsewhere stays here until somebody removes it here too, because
49
+ // an accidental deletion that propagates is unrecoverable.
50
+ return { outcome: OUTCOME.LOCAL_ONLY, hash: localHash };
51
+ }
52
+ if (localHash === remoteHash) return { outcome: OUTCOME.UNCHANGED, hash: localHash };
53
+
54
+ if (!base) {
55
+ // Both sides have it, they differ, and nothing says what they agreed on.
56
+ // Treating that as a conflict is the honest answer: neither side can claim
57
+ // the other's version is stale.
58
+ return { outcome: OUTCOME.BOTH, localHash, remoteHash };
59
+ }
60
+ const localMoved = localHash !== base.base_hash;
61
+ const remoteMoved = remoteHash !== base.base_hash;
62
+ if (localMoved && remoteMoved) return { outcome: OUTCOME.BOTH, localHash, remoteHash };
63
+ if (remoteMoved) return { outcome: OUTCOME.REMOTE_ONLY, hash: remoteHash };
64
+ if (localMoved) return { outcome: OUTCOME.LOCAL_ONLY, hash: localHash };
65
+ return { outcome: OUTCOME.UNCHANGED, hash: localHash };
66
+ }
67
+
68
+ /**
69
+ * Which fields actually differ, so a conflict names the disagreement rather
70
+ * than two opaque blobs. A reader deciding between two versions of a feature
71
+ * wants to know that the purpose moved, not that something did.
72
+ */
73
+ export function differingFields(local, remote) {
74
+ const keys = [...new Set([...Object.keys(local ?? {}), ...Object.keys(remote ?? {})])].sort();
75
+ const out = [];
76
+ for (const key of keys) {
77
+ // A version number differs by definition once anything else does, and
78
+ // saying so tells a reader nothing.
79
+ if (key === "version" || key === "updated_at") continue;
80
+ const a = local?.[key] ?? null;
81
+ const b = remote?.[key] ?? null;
82
+ if (JSON.stringify(a) !== JSON.stringify(b)) out.push({ field: key, local: a, remote: b });
83
+ }
84
+ return out;
85
+ }
86
+
87
+ /**
88
+ * Merge two versions field by field, taking the side that moved.
89
+ *
90
+ * Only usable when the two sides changed different fields, which is the common
91
+ * case for records with many independent columns. Where both moved the same
92
+ * field there is nothing to merge and the caller has to choose, so this reports
93
+ * what it could not settle rather than picking.
94
+ */
95
+ export function mergeFields({ local, remote, base }) {
96
+ const merged = { ...local };
97
+ const unsettled = [];
98
+ for (const { field, local: mine, remote: theirs } of differingFields(local, remote)) {
99
+ const original = base?.row?.[field] ?? undefined;
100
+ const localMoved = original === undefined || JSON.stringify(mine) !== JSON.stringify(original);
101
+ const remoteMoved = original === undefined || JSON.stringify(theirs) !== JSON.stringify(original);
102
+ if (localMoved && remoteMoved) {
103
+ unsettled.push(field);
104
+ continue;
105
+ }
106
+ if (remoteMoved) merged[field] = theirs;
107
+ }
108
+ return { merged, unsettled };
109
+ }
110
+
111
+ // ------------------------------------------------------------------- leases
112
+
113
+ /** How long a claim holds without being renewed. */
114
+ export const LEASE_MINUTES = 60;
115
+
116
+ export const leaseExpiry = (fromIso, minutes = LEASE_MINUTES) =>
117
+ new Date(Date.parse(fromIso) + minutes * 60 * 1000).toISOString();
118
+
119
+ /**
120
+ * Whether a remote lease still holds a task here.
121
+ *
122
+ * An expired lease is not a claim. A machine that crashed holding a task must
123
+ * not hold it forever, and asking a person to release it by hand is asking them
124
+ * to clean up after a failure they did not cause.
125
+ */
126
+ export function leaseHolds(lease, now = new Date().toISOString()) {
127
+ if (!lease?.lease_holder) return false;
128
+ if (!lease.lease_expires_at) return true;
129
+ return Date.parse(lease.lease_expires_at) > Date.parse(now);
130
+ }
131
+
132
+ /**
133
+ * Decide what a bundle's leases mean for this side.
134
+ *
135
+ * Returns the tasks this machine must not claim, each with the alias holding it
136
+ * and when that hold lapses, so a refusal can say who and until when instead of
137
+ * only no.
138
+ */
139
+ export function foreignLeases(bundle, selfAlias, now = new Date().toISOString()) {
140
+ return (bundle?.leases ?? [])
141
+ .filter((lease) => lease.holder !== selfAlias && leaseHolds(
142
+ { lease_holder: lease.holder, lease_expires_at: lease.expiresAt }, now))
143
+ .map((lease) => ({ taskId: lease.taskId, holder: lease.holder, expiresAt: lease.expiresAt }));
144
+ }
145
+
146
+ // ------------------------------------------------------------------- bundles
147
+
148
+ /**
149
+ * Everything this side is willing to send, already projected.
150
+ *
151
+ * The policy decides what is included; this only assembles it. Keeping those
152
+ * apart means a change to what may be shared is a change to one list, and the
153
+ * assembly cannot quietly disagree with it.
154
+ */
155
+ export async function buildBundle(db, { projectId, alias, tables }) {
156
+ const records = {};
157
+ for (const table of tables) {
158
+ if (!isShared(table)) continue;
159
+ let rows;
160
+ try {
161
+ rows = await db.all(`SELECT * FROM ${table}`);
162
+ } catch {
163
+ // A table this schema version does not have is not an error: two copies
164
+ // of a project can be one migration apart and still exchange everything
165
+ // they both understand.
166
+ continue;
167
+ }
168
+ records[table] = rows.map((row) => project(table, row));
169
+ }
170
+
171
+ const leases = await db.all(
172
+ `SELECT a.task_id, a.lease_holder, a.lease_expires_at
173
+ FROM task_assignments a
174
+ WHERE a.active = 1 AND a.lease_holder IS NOT NULL`).catch(() => []);
175
+
176
+ return {
177
+ format: 1,
178
+ project: projectId,
179
+ alias,
180
+ writtenAt: new Date().toISOString(),
181
+ records,
182
+ leases: leases.map((l) => ({
183
+ taskId: l.task_id, holder: l.lease_holder, expiresAt: l.lease_expires_at,
184
+ })),
185
+ };
186
+ }
@@ -0,0 +1,115 @@
1
+ // What may cross the boundary, and what never does. DEC-TBD-007.
2
+ //
3
+ // This is the one place that decides. Every other module asks it, so widening
4
+ // what is shared is a change to this list rather than a change scattered across
5
+ // a serializer, a merge and a report. Section 18 requires that a developer's
6
+ // identity and a project's existence are not disclosed across an organization
7
+ // boundary, and a rule spread over three files is a rule nobody can check.
8
+ //
9
+ // The list is what is allowed, never what is forbidden. A table added to the
10
+ // schema tomorrow is not shared until somebody adds it here and says why, which
11
+ // is the safe direction for a mistake to fall.
12
+
13
+ /**
14
+ * Tables that travel, in dependency order so a bundle can be applied top down
15
+ * without a foreign key failing.
16
+ *
17
+ * Specifications, decisions and evidence, which is what a colleague needs to
18
+ * understand what was agreed and what proved it. Tasks travel with them because
19
+ * an execution contract nobody can see is not shareable work, but they travel
20
+ * stripped: see PERSONAL below.
21
+ */
22
+ export const SHARED_TABLES = [
23
+ "projects",
24
+ "goals",
25
+ "milestones",
26
+ "modules",
27
+ "features",
28
+ "feature_flows",
29
+ "feature_acceptance_criteria",
30
+ "feature_edge_cases",
31
+ "workflows",
32
+ "workflow_steps",
33
+ "data_entities",
34
+ "data_fields",
35
+ "api_services",
36
+ "api_operations",
37
+ "surfaces",
38
+ "integrations",
39
+ "non_functional_requirements",
40
+ "schema_migrations",
41
+ "capability_areas",
42
+ "decisions",
43
+ "decision_links",
44
+ "test_plans",
45
+ "test_plan_cases",
46
+ "changes",
47
+ "change_targets",
48
+ "assumptions",
49
+ "tasks",
50
+ "task_contract_links",
51
+ "verification_evidence",
52
+ ];
53
+
54
+ /**
55
+ * Tables that never travel, with the reason, so a reader can see this was
56
+ * decided rather than overlooked.
57
+ */
58
+ export const WITHHELD = {
59
+ memory_entries: "A memory is one machine's recollection, not an agreed fact. DEC-TBD-007 keeps it local.",
60
+ memory_links: "Links into memory would describe the memory even without it.",
61
+ memory_search_terms: "An index of memory is memory.",
62
+ activity_events: "The activity trail records who did what and when, which is a working pattern.",
63
+ work_sessions: "A session says when a person was at their desk.",
64
+ developers: "A person's name and identity. Section 18 forbids disclosing it.",
65
+ agents: "Which harness and model somebody runs.",
66
+ task_assignments: "Who holds what. A lease crosses instead, carrying an alias rather than a person.",
67
+ branches: "Local branch names describe a working style and often a person.",
68
+ status_history: "A minute by minute record of how somebody worked.",
69
+ documents: "Generated files belong to the machine that generated them.",
70
+ source_material: "Intake material may contain anything the owner supplied.",
71
+ questions: "An open question is a conversation with the owner, not an agreed artifact.",
72
+ layout_positions: "Where somebody dragged a box on their own screen.",
73
+ sync_peers: "Who else this project syncs with. Section 18 forbids disclosing that another organization exists.",
74
+ sync_base: "A record of what a different peer holds.",
75
+ sync_conflicts: "A conflict is between two specific copies and means nothing to a third.",
76
+ applied_migrations: "The local schema history of one machine.",
77
+ };
78
+
79
+ /**
80
+ * Columns stripped from a row before it leaves, by table.
81
+ *
82
+ * A shared table can still carry a personal column. The row is worth sending
83
+ * and the column is not, so it is removed rather than the row withheld.
84
+ */
85
+ const PERSONAL = {
86
+ tasks: ["assignee_developer_id", "assignee_agent_id", "branch_id", "session_id"],
87
+ verification_evidence: ["recorded_by", "session_id"],
88
+ decisions: ["accepted_by"],
89
+ changes: ["requested_by", "decided_by", "session_id", "task_id"],
90
+ assumptions: ["recorded_by", "session_id"],
91
+ capability_areas: ["owner"],
92
+ };
93
+
94
+ /** Whether a table is shared at all. */
95
+ export const isShared = (table) => SHARED_TABLES.includes(table);
96
+
97
+ /**
98
+ * A row as it may leave this machine.
99
+ *
100
+ * Unknown columns are kept: a column added to a shared table is data about the
101
+ * product, and treating it as personal by default would silently drop product
102
+ * information. Personal columns are enumerated because they are the exception
103
+ * and each one was a judgement somebody made.
104
+ */
105
+ export function project(table, row) {
106
+ const strip = PERSONAL[table];
107
+ if (!strip) return { ...row };
108
+ const out = { ...row };
109
+ for (const column of strip) delete out[column];
110
+ return out;
111
+ }
112
+
113
+ /** Why a table is withheld, for a reader asking where their data went. */
114
+ export const withheldReason = (table) =>
115
+ WITHHELD[table] ?? "It is not on the list of what may be shared, and the list is what is allowed rather than what is forbidden.";