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,185 @@
1
+ // Stable, human-readable, typed identifiers.
2
+ //
3
+ // Ids appear in deep links, generated Markdown, commit messages and agent
4
+ // conversation, so they are readable rather than opaque. Uniqueness comes from
5
+ // the primary key: a colliding insert fails and the caller retries with the
6
+ // next number, which is safe because every write is already a transaction.
7
+
8
+ export const PREFIX = {
9
+ project: "PRJ",
10
+ project_scope_item: "SCOPE",
11
+ glossary_term: "TERM",
12
+ runtime_piece: "RT",
13
+ source_material: "SRC",
14
+ discovery_item: "DIS",
15
+ question: "Q",
16
+ goal: "GOAL",
17
+ goal_success_criterion: "GSC",
18
+ milestone: "MS",
19
+ module: "MOD",
20
+ feature: "FEAT",
21
+ feature_flow: "FLOW",
22
+ feature_acceptance_criterion: "AC",
23
+ feature_edge_case: "EC",
24
+ workflow: "WF",
25
+ workflow_actor: "WFA",
26
+ workflow_step: "STEP",
27
+ workflow_branch: "BR",
28
+ state_machine: "SM",
29
+ state: "ST",
30
+ state_transition: "STT",
31
+ surface: "SRF",
32
+ surface_state: "SRS",
33
+ ui_action: "ACT",
34
+ api_operation: "API",
35
+ data_entity: "ENT",
36
+ data_field: "FLD",
37
+ data_relationship: "REL",
38
+ schema_migration: "MIG",
39
+ integration: "INT",
40
+ job: "JOB",
41
+ webhook: "WH",
42
+ role: "ROLE",
43
+ permission: "PERM",
44
+ non_functional_requirement: "NFR",
45
+ decision: "DEC",
46
+ decision_link: "DL",
47
+ change: "CHG",
48
+ assumption: "ASM",
49
+ test_plan: "TP",
50
+ test_plan_case: "TPC",
51
+ api_service: "SVC",
52
+ decision_transition: "DT",
53
+ document: "DOC",
54
+ capability_area: "CAP",
55
+ module_completeness: "MCS",
56
+ task: "TASK",
57
+ task_category: "TC",
58
+ task_assignment: "ASG",
59
+ developer: "DEV",
60
+ agent: "AGT",
61
+ branch: "GBR",
62
+ work_session: "SES",
63
+ activity_event: "EVT",
64
+ verification_evidence: "EV",
65
+ status_history: "SH",
66
+ memory_entry: "MEM",
67
+ memory_link: "ML",
68
+ sync_peer: "PEER",
69
+ sync_conflict: "CONF",
70
+ layout_position: "POS",
71
+ };
72
+
73
+ const TABLE = {
74
+ project: "projects",
75
+ // Added by migration 002. runtime_piece_edges is deliberately absent: it has a
76
+ // composite primary key and no id column, like the other join tables here.
77
+ project_scope_item: "project_scope_items",
78
+ glossary_term: "glossary_terms",
79
+ runtime_piece: "runtime_pieces",
80
+ source_material: "source_material",
81
+ discovery_item: "discovery_items",
82
+ question: "questions",
83
+ goal: "goals",
84
+ goal_success_criterion: "goal_success_criteria",
85
+ milestone: "milestones",
86
+ module: "modules",
87
+ feature: "features",
88
+ feature_flow: "feature_flows",
89
+ feature_acceptance_criterion: "feature_acceptance_criteria",
90
+ feature_edge_case: "feature_edge_cases",
91
+ workflow: "workflows",
92
+ workflow_actor: "workflow_actors",
93
+ workflow_step: "workflow_steps",
94
+ workflow_branch: "workflow_branches",
95
+ state_machine: "state_machines",
96
+ state: "states",
97
+ state_transition: "state_transitions",
98
+ surface: "surfaces",
99
+ surface_state: "surface_states",
100
+ ui_action: "ui_actions",
101
+ api_operation: "api_operations",
102
+ data_entity: "data_entities",
103
+ data_field: "data_fields",
104
+ data_relationship: "data_relationships",
105
+ schema_migration: "schema_migrations",
106
+ integration: "integrations",
107
+ job: "jobs",
108
+ webhook: "webhooks",
109
+ role: "roles",
110
+ permission: "permissions",
111
+ non_functional_requirement: "non_functional_requirements",
112
+ decision: "decisions",
113
+ decision_link: "decision_links",
114
+ change: "changes",
115
+ assumption: "assumptions",
116
+ test_plan: "test_plans",
117
+ test_plan_case: "test_plan_cases",
118
+ api_service: "api_services",
119
+ decision_transition: "decision_transitions",
120
+ document: "documents",
121
+ capability_area: "capability_areas",
122
+ module_completeness: "module_completeness",
123
+ task: "tasks",
124
+ task_category: "task_categories",
125
+ task_assignment: "task_assignments",
126
+ developer: "developers",
127
+ agent: "agents",
128
+ branch: "branches",
129
+ work_session: "work_sessions",
130
+ activity_event: "activity_events",
131
+ verification_evidence: "verification_evidence",
132
+ status_history: "status_history",
133
+ memory_entry: "memory_entries",
134
+ memory_link: "memory_links",
135
+ sync_peer: "sync_peers",
136
+ sync_conflict: "sync_conflicts",
137
+ layout_position: "layout_positions",
138
+ };
139
+
140
+ export const tableFor = (kind) => TABLE[kind];
141
+
142
+ const PAD = 4;
143
+
144
+ /** `FEAT-0007`. Sequential per kind, derived from what is already stored. */
145
+ export async function nextId(db, kind) {
146
+ const prefix = PREFIX[kind];
147
+ const table = TABLE[kind];
148
+ if (!prefix || !table) throw new Error(`unknown record kind: ${kind}`);
149
+ const row = await db.get(
150
+ `SELECT id FROM ${table} WHERE id LIKE ? ORDER BY length(id) DESC, id DESC LIMIT 1`,
151
+ `${prefix}-%`,
152
+ );
153
+ const last = row ? Number(String(row.id).slice(prefix.length + 1)) : 0;
154
+ const next = (Number.isFinite(last) ? last : 0) + 1;
155
+ return `${prefix}-${String(next).padStart(PAD, "0")}`;
156
+ }
157
+
158
+ export function kindOf(id) {
159
+ const prefix = String(id).split("-")[0];
160
+ return Object.keys(PREFIX).find((k) => PREFIX[k] === prefix) ?? null;
161
+ }
162
+
163
+ export const isId = (value) => typeof value === "string" && /^[A-Z]{1,5}-\d{4,}$/.test(value);
164
+
165
+ /** URL and filesystem safe slug. Used for module and feature document paths. */
166
+ export function slugify(text, fallback = "item") {
167
+ const slug = String(text ?? "")
168
+ .toLowerCase()
169
+ .normalize("NFKD")
170
+ .replace(/[^\p{Letter}\p{Number}]+/gu, "-")
171
+ .replace(/^-+|-+$/g, "")
172
+ .slice(0, 60);
173
+ return slug || fallback;
174
+ }
175
+
176
+ export async function uniqueSlug(db, table, projectId, text, fallback) {
177
+ const base = slugify(text, fallback);
178
+ let candidate = base;
179
+ for (let n = 2; n < 500; n++) {
180
+ const clash = await db.get(`SELECT 1 FROM ${table} WHERE project_id = ? AND slug = ?`, projectId, candidate);
181
+ if (!clash) return candidate;
182
+ candidate = `${base}-${n}`;
183
+ }
184
+ throw new Error(`could not find a free slug for "${text}"`);
185
+ }
@@ -0,0 +1,150 @@
1
+ // Content screening applied at the storage boundary, so nothing reaches the
2
+ // database that the project would be embarrassed to generate: no em dash, no
3
+ // emoji, no secret-shaped strings, no absolute home paths, no model reasoning.
4
+ //
5
+ // Enforcement is real and refuses with the exact field, rather than advising.
6
+
7
+ export const E = {
8
+ EM_DASH: "E_STYLE_EM_DASH",
9
+ EMOJI: "E_STYLE_EMOJI",
10
+ SECRET: "E_SECRET_SHAPED",
11
+ HOME_PATH: "E_ABSOLUTE_HOME_PATH",
12
+ REASONING: "E_MODEL_REASONING_FIELD",
13
+ };
14
+
15
+ export class ScreeningError extends Error {
16
+ constructor(code, field, message) {
17
+ super(message);
18
+ this.name = "ScreeningError";
19
+ this.code = code;
20
+ this.field = field;
21
+ }
22
+ }
23
+
24
+ /** The character Superdev never generates. Exported so the style scanner, the
25
+ * storage boundary and the toolkit all test for the same codepoint. */
26
+ export const EM_DASH = "\u2014";
27
+ const EM_DASH_RE = new RegExp(EM_DASH);
28
+ // What counts as emoji, in one place, so the scanner and the storage boundary
29
+ // cannot disagree. The earlier range list banned the whole Arrows and Dingbats
30
+ // blocks, which made the store refuse an arrow or a check mark while the style
31
+ // validator passed them, and the Docs templates use both. A character in text
32
+ // presentation is text; emoji presentation is the thing being excluded.
33
+ const EMOJI_SOURCE = "\\p{Emoji_Presentation}|\\uFE0F";
34
+ const EMOJI = new RegExp(EMOJI_SOURCE, "u");
35
+ /** The same rule, global, for stripping. A non-global pattern removes one. */
36
+ const EMOJI_ALL = new RegExp(EMOJI_SOURCE, "gu");
37
+ /** Exported so the style validator judges by exactly the rule the store enforces. */
38
+ export const EMOJI_PATTERN = EMOJI_SOURCE;
39
+
40
+ const HOME_PATH = /(?:^|[\s"'`(=:])(\/(?:Users|home)\/[A-Za-z0-9._-]+|[A-Z]:\\Users\\[A-Za-z0-9._-]+)/;
41
+
42
+ const SECRET_PATTERNS = [
43
+ // The separator repeats, and it is either kind. Requiring sixteen unbroken
44
+ // alphanumerics after a single hyphen matched an OpenAI key of the older
45
+ // shape and missed every current one: sk-proj-... breaks at the second
46
+ // hyphen, and every Stripe key (sk_live_..., pk_test_...) uses underscores,
47
+ // so all three passed screening and would have been stored verbatim.
48
+ /\b(?:sk|pk|rk)[-_](?:[A-Za-z0-9]+[-_])*[A-Za-z0-9]{16,}\b/,
49
+ /\bgh[pousr]_[A-Za-z0-9]{20,}\b/,
50
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
51
+ /\bAKIA[0-9A-Z]{16}\b/,
52
+ /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/,
53
+ /\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/,
54
+ /\b(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?token)\s*[:=]\s*["']?[^\s"']{8,}/i,
55
+ ];
56
+
57
+ /** Fields that would carry private model reasoning. Never stored. */
58
+ const REASONING_FIELDS = new Set([
59
+ "reasoning", "chain_of_thought", "chainOfThought", "thinking", "thought",
60
+ "internal_monologue", "scratchpad", "deliberation",
61
+ ]);
62
+
63
+ /**
64
+ * Personal data shapes. Deliberately NOT part of the storage boundary: a project
65
+ * record may legitimately name a person, and refusing that would make the tool
66
+ * unusable. They exist for the outbound path only, where a report or a provider
67
+ * packet leaves the machine and a name that was fine on disk is a leak in transit.
68
+ */
69
+ const PII_PATTERNS = [
70
+ /[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net))[A-Za-z0-9.-]+\.[A-Za-z]{2,}/,
71
+ /(?:^|\s)\+?\d{1,3}[-. (]{1,2}\d{3}[-. )]{1,2}\d{3}[-. ]?\d{4}\b/,
72
+ ];
73
+
74
+ export const hasEmDash = (text) => EM_DASH_RE.test(String(text ?? ""));
75
+ export const hasEmoji = (text) => EMOJI.test(String(text ?? ""));
76
+ export const looksSecret = (text) => SECRET_PATTERNS.some((p) => p.test(String(text ?? "")));
77
+ export const looksPersonal = (text) => PII_PATTERNS.some((p) => p.test(String(text ?? "")));
78
+ export const hasHomePath = (text) => HOME_PATH.test(String(text ?? ""));
79
+
80
+ /** Would this field name become covert storage for model reasoning? */
81
+ export const isReasoningField = (key) => REASONING_FIELDS.has(key);
82
+
83
+ /**
84
+ * Rewrite punctuation rather than deleting the sentence. An em dash becomes a
85
+ * comma or a hyphen depending on how it was used.
86
+ */
87
+ export function rewriteStyle(text) {
88
+ if (typeof text !== "string") return text;
89
+ return text
90
+ .replace(/\s*\u2014\s*/g, " - ")
91
+ .replace(/\u2013/g, "-")
92
+ .replace(EMOJI_ALL, "")
93
+ .replace(/[ \t]{2,}/g, " ");
94
+ }
95
+
96
+ /** Replace an absolute home path with a portable placeholder. */
97
+ export const redactHomePaths = (text) =>
98
+ String(text ?? "")
99
+ .replace(/\/(?:Users|home)\/[A-Za-z0-9._-]+/g, "~")
100
+ .replace(/[A-Z]:\\Users\\[A-Za-z0-9._-]+/g, "~");
101
+
102
+ export function redactSecrets(text) {
103
+ let out = String(text ?? "");
104
+ for (const p of SECRET_PATTERNS) out = out.replace(new RegExp(p.source, p.flags.replace("g", "") + "g"), "[redacted]");
105
+ return out;
106
+ }
107
+
108
+ /** Throw on anything that must never be stored. Used on every write. */
109
+ export function assertStorable(field, value) {
110
+ if (value === null || value === undefined) return value;
111
+ if (typeof value !== "string") return value;
112
+ if (hasEmDash(value)) {
113
+ throw new ScreeningError(E.EM_DASH, field,
114
+ `${field} contains an em dash. Superdev never generates U+2014; use a comma, colon, semicolon, parentheses or a hyphen.`);
115
+ }
116
+ if (hasEmoji(value)) {
117
+ throw new ScreeningError(E.EMOJI, field, `${field} contains emoji, which Superdev never generates.`);
118
+ }
119
+ if (looksSecret(value)) {
120
+ throw new ScreeningError(E.SECRET, field, `${field} looks like it contains a credential. Store a reference, never the value.`);
121
+ }
122
+ if (hasHomePath(value)) {
123
+ throw new ScreeningError(E.HOME_PATH, field,
124
+ `${field} contains an absolute home path. It names one machine and points nowhere on anyone else's; store a path relative to the project.`);
125
+ }
126
+ return value;
127
+ }
128
+
129
+ /** Screen a whole record before it is written. Returns the record unchanged. */
130
+ export function assertRecordStorable(record, { allow = [] } = {}) {
131
+ for (const [key, value] of Object.entries(record ?? {})) {
132
+ if (REASONING_FIELDS.has(key)) {
133
+ throw new ScreeningError(E.REASONING, key, `${key} would store private model reasoning. Store the observable outcome instead.`);
134
+ }
135
+ // `allow` used to cover every column ending in _json, on the theory that
136
+ // structured data is not prose. None of these rules reacts to braces: an em
137
+ // dash inside a JSON array is the same character, and it was reaching
138
+ // generated Markdown through exactly this hole. A caller may still exempt a
139
+ // named field, but nothing is exempt by virtue of its shape.
140
+ if (allow.includes(key)) continue;
141
+ assertStorable(key, value);
142
+ }
143
+ return record;
144
+ }
145
+
146
+ /**
147
+ * Screen text arriving from a provider or an external file. External content is
148
+ * evidence, never instruction, so it is sanitized rather than refused.
149
+ */
150
+ export const sanitizeExternal = (text) => redactSecrets(rewriteStyle(redactHomePaths(text)));
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Shared safety layer for the command line surfaces.
3
+ *
4
+ * Extracted verbatim from the record engine that used to own it, because the
5
+ * guarantees below are the reason the surrounding commands are safe to run and
6
+ * rewriting them for tidiness would be a security change disguised as a cleanup:
7
+ *
8
+ * - root confinement: every path an operation touches is validated against the
9
+ * project root (absolute inputs rejected, drive letters rejected, ".."
10
+ * rejected, symlink escapes rejected) BEFORE any read or write
11
+ * - atomic writes: content lands via temp file, fsync, rename, directory fsync,
12
+ * so a crash leaves either the old file or the new one, never a torn one
13
+ * - deterministic serialization: stable key order and a trailing newline, so
14
+ * reruns produce byte-identical files
15
+ * - one shared refusal type, so a CLI can tell an expected refusal from a bug
16
+ *
17
+ * The screening and writing style rules are NOT implemented here. They live in
18
+ * screening.mjs, which is the storage boundary, and this file only adapts them
19
+ * to the shapes the command line callers use (whole structures rather than one
20
+ * field). Two implementations of "what counts as a secret" is how a rule quietly
21
+ * stops matching in one of the places it is enforced.
22
+ */
23
+ import crypto from "node:crypto";
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import { pathToFileURL } from "node:url";
27
+ import {
28
+ EM_DASH,
29
+ isReasoningField,
30
+ looksPersonal,
31
+ looksSecret,
32
+ redactHomePaths,
33
+ rewriteStyle,
34
+ } from "./screening.mjs";
35
+
36
+ export class SuperdevError extends Error {
37
+ constructor(code, message) {
38
+ super(message);
39
+ this.code = code;
40
+ }
41
+ }
42
+
43
+ /** Callers written against the previous name keep working unchanged. */
44
+ export { SuperdevError as TalksError };
45
+
46
+ /** Realpath-safe main-module check (Node realpaths the entry module URL). */
47
+ export function isMain(importMetaUrl) {
48
+ try {
49
+ return Boolean(process.argv[1]) && importMetaUrl === pathToFileURL(fs.realpathSync(process.argv[1])).href;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /** Resolve a project root: must exist, be a directory; returns its realpath. */
56
+ export function resolveRoot(root) {
57
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
58
+ throw new SuperdevError("E_ROOT", `root is not a directory: ${root}`);
59
+ return fs.realpathSync(root);
60
+ }
61
+
62
+ /**
63
+ * Resolve a project-relative path safely. Rejects absolute paths, drive-letter
64
+ * paths, ".." segments, resolution outside the root, and symlink escapes of any
65
+ * existing ancestor. Returns the absolute confined path.
66
+ */
67
+ export function safeResolve(rootReal, rel) {
68
+ const v = String(rel);
69
+ if (path.isAbsolute(v) || /^[A-Za-z]:[\\/]/.test(v))
70
+ throw new SuperdevError("E_PATH_ABSOLUTE", `absolute path rejected: field-controlled path`);
71
+ if (v.split(/[\\/]/).some((seg) => seg === ".."))
72
+ throw new SuperdevError("E_PATH_TRAVERSAL", `path traversal rejected`);
73
+ const resolved = path.resolve(rootReal, v);
74
+ if (resolved !== rootReal && !resolved.startsWith(rootReal + path.sep))
75
+ throw new SuperdevError("E_PATH_OUTSIDE", `path resolves outside root`);
76
+ // Walk existing ancestors: none may realpath outside the root.
77
+ let probe = resolved;
78
+ while (!fs.existsSync(probe)) probe = path.dirname(probe);
79
+ const real = fs.realpathSync(probe);
80
+ if (real !== rootReal && !real.startsWith(rootReal + path.sep))
81
+ throw new SuperdevError("E_PATH_SYMLINK", `symlink escape rejected`);
82
+ return resolved;
83
+ }
84
+
85
+ /** Interrupted-migration guard shared by every mutating command. The journal is
86
+ * written next to the store it belongs to and removed when the migration
87
+ * finishes, so its presence means a migration stopped part way through. */
88
+ export function migrationJournalPath(rootReal) {
89
+ return path.join(rootReal, ".superdev", ".migration-journal.json");
90
+ }
91
+
92
+ export function assertNotInterrupted(rootReal) {
93
+ if (fs.existsSync(migrationJournalPath(rootReal)))
94
+ throw new SuperdevError(
95
+ "E_MIGRATION_INTERRUPTED",
96
+ "an interrupted migration journal exists; complete or restore it before mutating the record"
97
+ );
98
+ }
99
+
100
+ /** Deterministic serialization: known keys in declared order first, remaining
101
+ * keys alphabetically, 2-space indent, trailing newline. */
102
+ export function stableStringify(value, preferredOrder = []) {
103
+ const order = (obj) => {
104
+ if (Array.isArray(obj)) return obj.map(order);
105
+ if (obj && typeof obj === "object") {
106
+ // Null prototype + hasOwn: fields named after Object.prototype members
107
+ // (toString, constructor, __proto__ and the rest) survive serialization intact.
108
+ const out = Object.create(null);
109
+ for (const k of preferredOrder) if (Object.hasOwn(obj, k)) out[k] = order(obj[k]);
110
+ for (const k of Object.keys(obj).sort()) if (!Object.hasOwn(out, k)) out[k] = order(obj[k]);
111
+ return out;
112
+ }
113
+ return obj;
114
+ };
115
+ return JSON.stringify(order(value), null, 2) + "\n";
116
+ }
117
+
118
+ /** Best-effort directory-entry durability (no-op where dirs cannot be fsynced,
119
+ * e.g. Windows). Scopes the durability claim: process-crash safety is
120
+ * guaranteed by construction; power-loss safety is best-effort via dir fsync. */
121
+ function fsyncDir(dir) {
122
+ try {
123
+ const fd = fs.openSync(dir, "r");
124
+ try {
125
+ fs.fsyncSync(fd);
126
+ } finally {
127
+ fs.closeSync(fd);
128
+ }
129
+ } catch {
130
+ /* platform without directory fsync */
131
+ }
132
+ }
133
+
134
+ /** Atomic write: temp file in the target directory, fsync, rename, dir fsync.
135
+ * Rename retries on EPERM (Windows: a concurrent reader briefly holding the
136
+ * target open makes rename flake; short bounded retry is the known remedy). */
137
+ export function atomicWrite(file, content) {
138
+ const dir = path.dirname(file);
139
+ fs.mkdirSync(dir, { recursive: true });
140
+ const tmp = path.join(dir, `.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);
141
+ const fd = fs.openSync(tmp, "w");
142
+ try {
143
+ fs.writeFileSync(fd, content);
144
+ fs.fsyncSync(fd);
145
+ } finally {
146
+ fs.closeSync(fd);
147
+ }
148
+ for (let attempt = 0; ; attempt++) {
149
+ try {
150
+ fs.renameSync(tmp, file);
151
+ break;
152
+ } catch (e) {
153
+ if (e.code === "EPERM" && attempt < 4) {
154
+ const until = Date.now() + 15;
155
+ while (Date.now() < until); // bounded busy-wait; sync CLI context
156
+ continue;
157
+ }
158
+ fs.rmSync(tmp, { force: true });
159
+ throw e;
160
+ }
161
+ }
162
+ fsyncDir(dir);
163
+ }
164
+
165
+ /** ISO-8601 UTC timestamp, second precision, filename-safe when compacted. */
166
+ export function nowStamp() {
167
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
168
+ }
169
+
170
+ /** Shared CLI reporting contract (every public CLI):
171
+ * - `--out <file>` writes exactly the machine-readable report, deterministic
172
+ * JSON with a trailing newline, via the atomic path; no other destination is
173
+ * ever invented.
174
+ * - absence of --out writes no report file; read-only ops honor --out too.
175
+ * - expected failures never print stack traces. */
176
+ export function writeReport(outPath, report) {
177
+ if (!outPath) return;
178
+ atomicWrite(outPath, JSON.stringify(report, null, 2) + "\n");
179
+ }
180
+
181
+ export function usageError(message, usage) {
182
+ console.error(message);
183
+ console.error(usage);
184
+ process.exit(2);
185
+ }
186
+
187
+ /* ------------------------------------------------------------------ *
188
+ * Screening and writing style: adapters over screening.mjs
189
+ * ------------------------------------------------------------------ */
190
+
191
+ export { EM_DASH };
192
+
193
+ /** Rewrite prohibited punctuation rather than deleting the sentence around it.
194
+ * Used for migration and for provider output, never as a silent fixer on the
195
+ * write path: a write that would have been wrong should fail loudly while
196
+ * someone can still choose the right punctuation. */
197
+ export const rewriteWritingStyle = rewriteStyle;
198
+
199
+ const isSensitiveString = (str) => looksSecret(str) || looksPersonal(str);
200
+
201
+ /** Is any string in this value secret- or personal-shaped? Never returns the value. */
202
+ export function containsSensitive(value) {
203
+ let hit = false;
204
+ const walk = (v) => {
205
+ if (hit) return;
206
+ if (typeof v === "string") hit = isSensitiveString(v);
207
+ else if (Array.isArray(v)) v.forEach(walk);
208
+ else if (v && typeof v === "object") Object.values(v).forEach(walk);
209
+ };
210
+ walk(value);
211
+ return hit;
212
+ }
213
+
214
+ /** Redact every sensitive or machine-identifying string in a value, recursively.
215
+ * Returns a NEW value; the offending text is replaced, never echoed. Arrays,
216
+ * nested objects and object KEYS are all covered. */
217
+ export function redactSensitive(value, { onFinding } = {}) {
218
+ const redactString = (str) => {
219
+ if (isSensitiveString(str)) {
220
+ onFinding?.("sensitive");
221
+ return "[redacted: sensitive shape]";
222
+ }
223
+ const portable = redactHomePaths(str);
224
+ if (portable !== str) onFinding?.("machine-path");
225
+ return portable;
226
+ };
227
+ const walk = (v) => {
228
+ if (typeof v === "string") return redactString(v);
229
+ if (Array.isArray(v)) return v.map(walk);
230
+ if (v && typeof v === "object") {
231
+ const out = {};
232
+ for (const [k, val] of Object.entries(v)) out[redactString(k)] = walk(val);
233
+ return out;
234
+ }
235
+ return v;
236
+ };
237
+ return walk(value);
238
+ }
239
+
240
+ /** The single shared field-safety gate: reject any secret or personal string AND
241
+ * any reasoning-shaped field name, anywhere in the value. Coded refusal that
242
+ * NEVER echoes the offending value, so the refusal itself cannot leak it. */
243
+ export function assertSafeField(value, where = "field") {
244
+ const walk = (v) => {
245
+ if (typeof v === "string") {
246
+ if (isSensitiveString(v)) throw new SuperdevError("E_SENSITIVE", `secret or personal content rejected in ${where}`);
247
+ } else if (Array.isArray(v)) {
248
+ v.forEach(walk);
249
+ } else if (v && typeof v === "object") {
250
+ for (const [k, val] of Object.entries(v)) {
251
+ if (isReasoningField(k))
252
+ throw new SuperdevError("E_REASONING_FIELD", `reasoning-shaped field "${k}" rejected in ${where}`);
253
+ walk(val);
254
+ }
255
+ }
256
+ };
257
+ walk(value);
258
+ }