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.
- package/.claude-plugin/marketplace.json +33 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.codex-plugin/plugin.json +27 -0
- package/CODE_OF_CONDUCT.md +109 -0
- package/CONTRIBUTING.md +205 -0
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +1051 -0
- package/SECURITY.md +135 -0
- package/THIRD-PARTY-NOTICES.md +253 -0
- package/hooks/hooks.json +61 -0
- package/hooks/run.mjs +88 -0
- package/package.json +65 -0
- package/references/confidentiality.md +48 -0
- package/references/evidence-and-risk.md +73 -0
- package/references/operating-model.md +105 -0
- package/references/platform-capabilities.md +51 -0
- package/references/project-record.md +91 -0
- package/references/provider-contracts.md +102 -0
- package/scripts/doctor/doctor.mjs +539 -0
- package/scripts/privacy/scan-history.mjs +163 -0
- package/scripts/privacy/scan.mjs +368 -0
- package/scripts/validate/README.md +94 -0
- package/scripts/validate/common.mjs +149 -0
- package/scripts/validate/data-model.mjs +225 -0
- package/scripts/validate/dependencies.mjs +69 -0
- package/scripts/validate/docs-templates.mjs +99 -0
- package/scripts/validate/footprint.mjs +78 -0
- package/scripts/validate/imports.mjs +61 -0
- package/scripts/validate/manifests.mjs +173 -0
- package/scripts/validate/markdown.mjs +123 -0
- package/scripts/validate/migrations.mjs +187 -0
- package/scripts/validate/no-tests.mjs +136 -0
- package/scripts/validate/privacy.mjs +115 -0
- package/scripts/validate/record-links.mjs +127 -0
- package/scripts/validate/skill-commands.mjs +190 -0
- package/scripts/validate/skills.mjs +112 -0
- package/scripts/validate/specification.mjs +88 -0
- package/scripts/validate/style.mjs +78 -0
- package/scripts/validate/validate-all.mjs +175 -0
- package/skills/debug/SKILL.md +68 -0
- package/skills/decision/SKILL.md +99 -0
- package/skills/docs/SKILL.md +86 -0
- package/skills/docs/assets/fragments/api/events.md +14 -0
- package/skills/docs/assets/fragments/api/graphql.md +14 -0
- package/skills/docs/assets/fragments/api/local-only.md +13 -0
- package/skills/docs/assets/fragments/api/rest.md +13 -0
- package/skills/docs/assets/fragments/api/rpc.md +13 -0
- package/skills/docs/assets/fragments/async/event-bus.md +13 -0
- package/skills/docs/assets/fragments/async/none.md +11 -0
- package/skills/docs/assets/fragments/async/platform-jobs.md +13 -0
- package/skills/docs/assets/fragments/async/queue.md +13 -0
- package/skills/docs/assets/fragments/async/scheduler.md +13 -0
- package/skills/docs/assets/fragments/auth/detected-provider.md +15 -0
- package/skills/docs/assets/fragments/auth/neutral-base.md +14 -0
- package/skills/docs/assets/fragments/data/document.md +13 -0
- package/skills/docs/assets/fragments/data/external-saas.md +13 -0
- package/skills/docs/assets/fragments/data/key-value.md +13 -0
- package/skills/docs/assets/fragments/data/none.md +12 -0
- package/skills/docs/assets/fragments/data/sql-orm.md +13 -0
- package/skills/docs/assets/fragments/data/sql-plain.md +13 -0
- package/skills/docs/assets/fragments/env/conventional.md +13 -0
- package/skills/docs/assets/fragments/env/envx.md +13 -0
- package/skills/docs/assets/fragments/env/managed-platform.md +13 -0
- package/skills/docs/assets/fragments/env/none.md +11 -0
- package/skills/docs/assets/fragments/ui/api-only.md +13 -0
- package/skills/docs/assets/fragments/ui/cli.md +14 -0
- package/skills/docs/assets/fragments/ui/desktop.md +14 -0
- package/skills/docs/assets/fragments/ui/mobile.md +14 -0
- package/skills/docs/assets/fragments/ui/web.md +14 -0
- package/skills/docs/assets/templates/adr.md +62 -0
- package/skills/docs/assets/templates/api.md +30 -0
- package/skills/docs/assets/templates/architecture.md +52 -0
- package/skills/docs/assets/templates/change-impact-drift-report.md +29 -0
- package/skills/docs/assets/templates/compliance.md +29 -0
- package/skills/docs/assets/templates/data-schema.md +43 -0
- package/skills/docs/assets/templates/feature.md +43 -0
- package/skills/docs/assets/templates/foundations.md +55 -0
- package/skills/docs/assets/templates/jobs-webhooks.md +36 -0
- package/skills/docs/assets/templates/module-inventory.md +19 -0
- package/skills/docs/assets/templates/module.md +50 -0
- package/skills/docs/assets/templates/nfr.md +22 -0
- package/skills/docs/assets/templates/observability.md +28 -0
- package/skills/docs/assets/templates/pages-ui-actions.md +47 -0
- package/skills/docs/assets/templates/project-summary.md +44 -0
- package/skills/docs/assets/templates/roles-permissions.md +26 -0
- package/skills/docs/assets/templates/test-plan.md +23 -0
- package/skills/docs/assets/templates/workflow-state-machine.md +43 -0
- package/skills/docs/references/adr-authoring.md +24 -0
- package/skills/docs/references/apis-and-data.md +23 -0
- package/skills/docs/references/capability-fragments.md +25 -0
- package/skills/docs/references/change-tracking.md +62 -0
- package/skills/docs/references/diagrams.md +31 -0
- package/skills/docs/references/discovery.md +59 -0
- package/skills/docs/references/edge-cases.md +45 -0
- package/skills/docs/references/foundations-modules.md +18 -0
- package/skills/docs/references/ingestion.md +52 -0
- package/skills/docs/references/initialize-adopt.md +25 -0
- package/skills/docs/references/module-decomposition.md +38 -0
- package/skills/docs/references/profiles.md +60 -0
- package/skills/docs/references/quality-attributes.md +27 -0
- package/skills/docs/references/reverse-engineer.md +21 -0
- package/skills/docs/references/spec-depths.md +31 -0
- package/skills/docs/references/summarize.md +18 -0
- package/skills/docs/references/surfaces-and-actions.md +24 -0
- package/skills/docs/references/validation.md +47 -0
- package/skills/docs/references/workflows-and-jobs.md +25 -0
- package/skills/docs/scripts/ingest.mjs +984 -0
- package/skills/docs/scripts/profile-detect.mjs +299 -0
- package/skills/docs/scripts/screen.mjs +96 -0
- package/skills/docs/scripts/template-lint.mjs +143 -0
- package/skills/docs/scripts/validate-docs.mjs +363 -0
- package/skills/doctor/SKILL.md +167 -0
- package/skills/feature/SKILL.md +132 -0
- package/skills/init/SKILL.md +137 -0
- package/skills/project/SKILL.md +261 -0
- package/skills/project/references/commands.md +213 -0
- package/skills/resume/SKILL.md +79 -0
- package/skills/review/SKILL.md +91 -0
- package/skills/status/SKILL.md +85 -0
- package/skills/task/SKILL.md +183 -0
- package/src/cli/product-map.mjs +468 -0
- package/src/cli/render.mjs +544 -0
- package/src/cli.mjs +2774 -0
- package/src/cloud/crypto.mjs +118 -0
- package/src/cloud/merge.mjs +186 -0
- package/src/cloud/policy.mjs +115 -0
- package/src/cloud/sync.mjs +512 -0
- package/src/cloud/transport.mjs +116 -0
- package/src/db/connect.mjs +189 -0
- package/src/db/maintenance.mjs +419 -0
- package/src/db/migrate.mjs +188 -0
- package/src/db/migrations/001_initial.sql +1024 -0
- package/src/db/migrations/002_docs_coverage.sql +126 -0
- package/src/db/migrations/003_memory_retrieval_and_integrity.sql +168 -0
- package/src/db/migrations/004_task_categories.sql +59 -0
- package/src/db/migrations/005_executable_evidence.sql +32 -0
- package/src/db/migrations/006_module_and_feature_boundaries.sql +27 -0
- package/src/db/migrations/007_drop_redundant_module_users.sql +13 -0
- package/src/db/migrations/008_changes_assumptions_test_plans_api_services.sql +166 -0
- package/src/db/migrations/009_retired_documents.sql +54 -0
- package/src/db/migrations/010_test_plan_evidence.sql +20 -0
- package/src/db/migrations/011_criterion_waiver.sql +13 -0
- package/src/db/migrations/012_synchronization.sql +56 -0
- package/src/db/store.mjs +327 -0
- package/src/decisions/record.mjs +167 -0
- package/src/docs/proposals.mjs +871 -0
- package/src/docs/render.mjs +1424 -0
- package/src/docs/templates.mjs +1281 -0
- package/src/features/acceptance.mjs +315 -0
- package/src/features/specify.mjs +248 -0
- package/src/init/discovery.mjs +901 -0
- package/src/init/index.mjs +784 -0
- package/src/init/questions.mjs +562 -0
- package/src/memory/benchmark.mjs +171 -0
- package/src/memory/capture.mjs +135 -0
- package/src/memory/consolidate.mjs +255 -0
- package/src/memory/index.mjs +810 -0
- package/src/model/ids.mjs +185 -0
- package/src/model/screening.mjs +150 -0
- package/src/model/toolkit.mjs +258 -0
- package/src/model/vocabulary.mjs +190 -0
- package/src/product/assumptions.mjs +120 -0
- package/src/product/changes.mjs +180 -0
- package/src/product/test-plans.mjs +183 -0
- package/src/progress/index.mjs +1364 -0
- package/src/runtime/harness.mjs +264 -0
- package/src/runtime/hooks.mjs +1021 -0
- package/src/runtime/identity.mjs +305 -0
- package/src/runtime/resume.mjs +343 -0
- package/src/runtime/session.mjs +679 -0
- package/src/runtime/version.mjs +315 -0
- package/src/service/assets/control-center.html +206 -0
- package/src/service/assets/control-center.manifest.json +7 -0
- package/src/service/manage.mjs +542 -0
- package/src/service/mutations.mjs +860 -0
- package/src/service/read-model.mjs +1557 -0
- package/src/service/server.mjs +783 -0
- package/src/tasks/categories.mjs +154 -0
- package/src/tasks/derive.mjs +977 -0
- package/src/tasks/lifecycle.mjs +830 -0
- 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
|
+
}
|