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,190 @@
1
+ // The words the product uses. Data, not UI constants: categories and capability
2
+ // areas are seeded as rows so a project can add its own.
3
+
4
+ /** Brief section 7.4. Order is lifecycle order, used for board columns. */
5
+ export const TASK_STATUSES = [
6
+ "draft", "ready", "in_progress", "in_review", "verifying",
7
+ "blocked", "paused", "complete", "cancelled", "superseded",
8
+ ];
9
+
10
+ export const OPEN_TASK_STATUSES = TASK_STATUSES.filter(
11
+ (s) => !["complete", "cancelled", "superseded"].includes(s),
12
+ );
13
+
14
+ export const TERMINAL_TASK_STATUSES = ["complete", "cancelled", "superseded"];
15
+
16
+ /** Human-facing label. Title Case, plain language, no internal vocabulary. */
17
+ export const LABEL = {
18
+ draft: "Draft",
19
+ ready: "Ready",
20
+ in_progress: "In Progress",
21
+ in_review: "In Review",
22
+ verifying: "Verifying",
23
+ blocked: "Blocked",
24
+ paused: "Paused",
25
+ complete: "Complete",
26
+ cancelled: "Cancelled",
27
+ superseded: "Superseded",
28
+ planned: "Planned",
29
+ implemented: "Implemented",
30
+ deprecated: "Deprecated",
31
+ accepted: "Accepted",
32
+ proposed: "Proposed",
33
+ rejected: "Rejected",
34
+ specified: "Specified",
35
+ awaiting_decision: "Awaiting Decision",
36
+ not_applicable: "Not Applicable",
37
+ deferred: "Deferred",
38
+ met: "Met",
39
+ unmet: "Unmet",
40
+ unmeasured: "Unmeasured",
41
+ open: "Open",
42
+ filled: "Filled",
43
+ answered: "Answered",
44
+ };
45
+
46
+ export const titleCase = (value) =>
47
+ LABEL[value] ??
48
+ String(value ?? "")
49
+ .replace(/[_-]+/g, " ")
50
+ .replace(/\b\w/g, (c) => c.toUpperCase());
51
+
52
+ /**
53
+ * Brief section 7.3, plus the categories the derivation inputs in 11.1 actually
54
+ * produce. Seeded per project, and projects may add their own: these are a
55
+ * starting vocabulary, not a closed set.
56
+ *
57
+ * The later entries exist because derivation was otherwise forced to file real
58
+ * work under a category that did not describe it. Proving rollout and rollback
59
+ * is not Infrastructure, and a surface with its actions is not a generic
60
+ * Feature.
61
+ */
62
+ export const SYSTEM_TASK_CATEGORIES = [
63
+ "Feature", "Improvement", "Bug", "Refactor", "Investigation", "Documentation",
64
+ "Integration", "Migration", "Infrastructure", "Security", "Quality", "Backlog",
65
+ "Design", "Testing", "Performance", "Accessibility", "Observability",
66
+ "Data", "Release", "Compliance", "Chore",
67
+ ];
68
+
69
+ /**
70
+ * One line per category, so the interface can say what a category means rather
71
+ * than assuming everyone reads the same meaning into a single word. Shown in
72
+ * the category picker and in filter help.
73
+ */
74
+ export const TASK_CATEGORY_MEANINGS = {
75
+ Feature: "New product behavior somebody asked for.",
76
+ Improvement: "Existing behavior made better without changing what it is.",
77
+ Bug: "Behavior that does not match what was accepted.",
78
+ Refactor: "Internal change with no change in behavior.",
79
+ Investigation: "Finding something out before deciding what to build.",
80
+ Documentation: "Keeping the written record in step with the product.",
81
+ Integration: "Work at the boundary with something outside this product.",
82
+ Migration: "Moving data or schema from one shape to another.",
83
+ Infrastructure: "The ground the product runs on.",
84
+ Security: "Protecting the product, its data and the people in it.",
85
+ Quality: "Work that raises confidence rather than adding behavior.",
86
+ Backlog: "Agreed as worth doing, not yet scheduled.",
87
+ Design: "Surfaces, actions and how the product presents itself.",
88
+ Testing: "Product tests derived from the accepted test plan.",
89
+ Performance: "Meeting a declared speed or capacity target.",
90
+ Accessibility: "Making the product operable by everyone who uses it.",
91
+ Observability: "Signals that prove the product is working in the open.",
92
+ Data: "Modelling the entities and relationships the product owns.",
93
+ Release: "Getting the work out, and being able to take it back.",
94
+ Compliance: "Obligations under a regime this project has declared.",
95
+ Chore: "Routine upkeep that keeps the project healthy.",
96
+ };
97
+
98
+ /** Brief section 8.2. Every area is answered or explicitly not applicable. */
99
+ export const CAPABILITY_AREAS = [
100
+ "Product purpose and success criteria",
101
+ "Users, roles, permissions and tenancy",
102
+ "Frontend delivery shape",
103
+ "Navigation and information architecture",
104
+ "Design system and accessibility",
105
+ "Backend boundaries and service responsibilities",
106
+ "API style and public contracts",
107
+ "Authentication and session lifecycle",
108
+ "Authorization enforcement",
109
+ "Database and data ownership",
110
+ "Data migrations and rollback",
111
+ "File or object storage",
112
+ "Search and indexing",
113
+ "Real-time behavior",
114
+ "Offline behavior and conflict handling",
115
+ "Background jobs and scheduling",
116
+ "Events and webhooks",
117
+ "External integrations",
118
+ "Notifications",
119
+ "Rate limiting and abuse controls",
120
+ "Security and privacy",
121
+ "Compliance",
122
+ "Observability and operational response",
123
+ "Performance and capacity targets",
124
+ "Environments and secret management",
125
+ "Continuous integration and delivery",
126
+ "Infrastructure and deployment",
127
+ "Backups, recovery, retention and deletion",
128
+ "Product analytics",
129
+ "Testing strategy for the product",
130
+ "Release and rollback",
131
+ ];
132
+
133
+ /** Brief section 9.3, the twenty-step module loop. */
134
+ export const MODULE_STEPS = [
135
+ "Pages and surfaces", "UI composition", "Actions", "API surface", "Data",
136
+ "End-to-end wiring", "State machines", "Events", "Edge cases", "UI states",
137
+ "Telemetry", "Accessibility", "Internationalization", "Feature flags",
138
+ "Responsive behavior", "User-facing copy", "URL state and deep links",
139
+ "Performance", "Discoverability and SEO", "Compliance and product tests",
140
+ ];
141
+
142
+ /** Docs edge-case categories. Each is specified or deliberately not applicable. */
143
+ export const EDGE_CASE_CATEGORIES = [
144
+ "empty_states", "boundary_values", "invalid_input", "permission_boundaries",
145
+ "state_machine_violations", "concurrent_actions", "ordering", "duplication",
146
+ "network_failure", "dependency_failure", "slow_paths", "time",
147
+ "data_migration_states", "recovery", "limits_and_quotas",
148
+ "multi_device_session", "offline_degraded", "localization",
149
+ "platform_variance", "versioning", "deletion_semantics", "consistency",
150
+ "privacy_lifecycle", "auditability",
151
+ ];
152
+
153
+ export const ACTIVITY_EVENT_TYPES = [
154
+ "task_created", "task_claimed", "task_started", "task_updated", "task_blocked",
155
+ "task_unblocked", "verification_attached", "task_completed", "task_reopened",
156
+ "task_released", "task_cancelled", "decision_created", "decision_transitioned",
157
+ "specification_changed", "code_changed", "documentation_generated",
158
+ "documentation_proposal_raised", "documentation_proposal_resolved",
159
+ "session_started", "session_compacted", "session_handed_off", "session_ended",
160
+ "project_initialized", "scope_changed", "tasks_derived",
161
+ ];
162
+
163
+ export const SPEC_DEPTHS = ["microspec", "standard", "full"];
164
+
165
+ /** What each depth must carry before it can be accepted. Brief section 9.2. */
166
+ export const DEPTH_REQUIREMENTS = {
167
+ microspec: ["purpose", "user_statement", "scope", "flow", "acceptance_criteria", "edge_cases"],
168
+ standard: ["purpose", "user_statement", "scope", "flow", "acceptance_criteria", "edge_cases",
169
+ "surfaces", "api_or_data", "workflow", "observability", "test_plan"],
170
+ full: ["purpose", "user_statement", "scope", "flow", "acceptance_criteria", "edge_cases",
171
+ "surfaces", "api_or_data", "workflow", "observability", "test_plan",
172
+ "decision", "migration_or_rollback", "security_privacy"],
173
+ };
174
+
175
+ /**
176
+ * `3 features`, `1 feature`, `2 categories`.
177
+ *
178
+ * The one place a count meets its noun. Superdev used to write `feature(s)`
179
+ * everywhere, which is a machine hedging about grammar it already has the
180
+ * number for, and it produced sentences no person would write: "1 verification
181
+ * record(s) are out of date", where the count is singular and the verb is not.
182
+ * Irregular plurals are passed in rather than guessed, because "criterion(s)"
183
+ * was the other half of the same problem and the plural is "criteria".
184
+ */
185
+ export function count(n, singular, many = `${singular}s`) {
186
+ return `${n} ${n === 1 ? singular : many}`;
187
+ }
188
+
189
+ /** The same choice without the number, for a verb that has to agree with it. */
190
+ export const agrees = (n, one, many) => (n === 1 ? one : many);
@@ -0,0 +1,120 @@
1
+ // Reversible answers, and what would make each worth revisiting.
2
+ //
3
+ // Section 8.4 says "I do not know" is a valid answer, that Superdev may
4
+ // recommend a reversible assumption, and that it must record the assumption
5
+ // and its review trigger. Without a place to put one, an assumption becomes an
6
+ // undocumented guess: nobody knows it was assumed rather than decided, and
7
+ // nobody knows when to check it again.
8
+ //
9
+ // The review trigger is required for exactly that reason. An assumption with no
10
+ // trigger is never reviewed and quietly hardens into a fact nobody chose.
11
+
12
+ import { create, mutate, query, setStatus } from "../db/store.mjs";
13
+ import { assertStorable } from "../model/screening.mjs";
14
+
15
+ export const E = { NOT_FOUND: "E_NOT_FOUND", INVALID: "E_INVALID" };
16
+
17
+ export class AssumptionError extends Error {
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.name = "AssumptionError";
21
+ this.code = code;
22
+ }
23
+ }
24
+
25
+ export const ASSUMPTION_STATUSES = ["holding", "confirmed", "overturned", "expired"];
26
+
27
+ /**
28
+ * Record an assumption.
29
+ *
30
+ * Three things are required and each earns its place: the statement, because
31
+ * an unstated assumption cannot be checked; why it was assumed rather than
32
+ * decided, because that is what separates it from a decision nobody wrote down;
33
+ * and the review trigger, because without one it is never revisited.
34
+ */
35
+ export async function recordAssumption(root, input = {}) {
36
+ const statement = String(input.statement ?? "").trim();
37
+ const whyAssumed = String(input.whyAssumed ?? "").trim();
38
+ const reviewTrigger = String(input.reviewTrigger ?? "").trim();
39
+
40
+ if (!statement) throw new AssumptionError(E.INVALID, "An assumption needs its statement.");
41
+ if (!whyAssumed) {
42
+ throw new AssumptionError(E.INVALID,
43
+ "Say why this is assumed rather than decided. Without that it reads as a decision nobody recorded.");
44
+ }
45
+ if (!reviewTrigger) {
46
+ throw new AssumptionError(E.INVALID,
47
+ "An assumption needs its review trigger. Without one nobody knows when to check it, and it hardens into a fact nobody chose.");
48
+ }
49
+
50
+ return mutate(root, async (db) => {
51
+ const project = await db.get("SELECT id FROM projects LIMIT 1");
52
+ if (!project) throw new AssumptionError(E.NOT_FOUND, "There is no project to record an assumption against.");
53
+
54
+ if (input.questionId) {
55
+ const q = await db.get("SELECT id FROM questions WHERE id = ?", input.questionId);
56
+ if (!q) throw new AssumptionError(E.NOT_FOUND, `There is no question ${input.questionId}.`);
57
+ }
58
+
59
+ return create(db, "assumption", {
60
+ project_id: project.id,
61
+ statement: assertStorable("statement", statement),
62
+ why_assumed: assertStorable("why_assumed", whyAssumed),
63
+ review_trigger: assertStorable("review_trigger", reviewTrigger),
64
+ consequence_if_wrong: input.consequenceIfWrong
65
+ ? assertStorable("consequence_if_wrong", String(input.consequenceIfWrong)) : null,
66
+ scope_type: input.scopeType ?? null,
67
+ scope_id: input.scopeId ?? null,
68
+ question_id: input.questionId ?? null,
69
+ status: "holding",
70
+ }, {
71
+ projectId: project.id,
72
+ actor: input.actor ?? "superdev",
73
+ sessionId: input.sessionId ?? null,
74
+ activityType: "assumption_recorded",
75
+ activitySummary: `Assumption recorded: ${statement}`.slice(0, 200),
76
+ });
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Resolve an assumption, saying what it turned out to be.
82
+ *
83
+ * Confirmed and overturned are both resolutions worth recording. An overturned
84
+ * assumption that leaves no trace is how a project forgets that it once
85
+ * believed something and acted on it.
86
+ */
87
+ export async function resolveAssumption(root, assumptionId, { to, resolution, actor = "superdev" } = {}) {
88
+ if (!ASSUMPTION_STATUSES.includes(to)) {
89
+ throw new AssumptionError(E.INVALID,
90
+ `"${to}" is not an assumption status. Use one of: ${ASSUMPTION_STATUSES.join(", ")}.`);
91
+ }
92
+ if (to !== "holding" && !String(resolution ?? "").trim()) {
93
+ throw new AssumptionError(E.INVALID,
94
+ "Say what it turned out to be. A resolved assumption with no resolution loses the answer it was standing in for.");
95
+ }
96
+
97
+ return mutate(root, async (db) => {
98
+ const row = await db.get("SELECT * FROM assumptions WHERE id = ?", assumptionId);
99
+ if (!row) throw new AssumptionError(E.NOT_FOUND, `There is no assumption ${assumptionId}.`);
100
+ if (row.status === to) {
101
+ throw new AssumptionError(E.INVALID, `${assumptionId} is already ${to}.`);
102
+ }
103
+ await db.run(
104
+ "UPDATE assumptions SET resolution = ?, resolved_by = ?, resolved_at = ? WHERE id = ?",
105
+ resolution ? assertStorable("resolution", String(resolution)) : null,
106
+ actor, new Date().toISOString(), assumptionId,
107
+ );
108
+ return setStatus(db, "assumption", assumptionId, to, { actor, reason: resolution ?? null });
109
+ });
110
+ }
111
+
112
+ /** Every assumption, the ones still holding first because those are live. */
113
+ export async function listAssumptions(root, { status = null } = {}) {
114
+ return query(root, (db) => db.all(
115
+ `SELECT * FROM assumptions
116
+ ${status ? "WHERE status = ?" : ""}
117
+ ORDER BY CASE status WHEN 'holding' THEN 0 ELSE 1 END, created_at DESC`,
118
+ ...(status ? [status] : []),
119
+ ));
120
+ }
@@ -0,0 +1,180 @@
1
+ // Recording what moved in accepted scope, and why.
2
+ //
3
+ // Section 6.2 says a Change must record the affected records and the reason,
4
+ // and 14.2 says changes must preserve audit history. An activity event records
5
+ // that something happened; a change records that accepted scope moved, which is
6
+ // the claim a reader comes back for months later when asking why the product is
7
+ // not what the plan said.
8
+ //
9
+ // A change is append-only, enforced by a trigger. Recording one wrongly is
10
+ // corrected by recording another that says so, never by editing the first.
11
+
12
+ import { create, mutate, query } from "../db/store.mjs";
13
+ import { assertStorable } from "../model/screening.mjs";
14
+
15
+ export const E = {
16
+ NOT_FOUND: "E_NOT_FOUND",
17
+ INVALID: "E_INVALID",
18
+ };
19
+
20
+ export class ChangeError extends Error {
21
+ constructor(code, message, detail = {}) {
22
+ super(message);
23
+ this.name = "ChangeError";
24
+ this.code = code;
25
+ this.detail = detail;
26
+ }
27
+ }
28
+
29
+ /** The kinds a change can be, in the vocabulary the schema accepts. */
30
+ export const CHANGE_TYPES = [
31
+ "scope_added", "scope_removed", "behavior_changed",
32
+ "contract_changed", "correction", "scope_changed",
33
+ ];
34
+
35
+ const asList = (value) =>
36
+ (value === null || value === undefined ? [] : [].concat(value))
37
+ .map((v) => String(v).trim()).filter(Boolean);
38
+
39
+ /**
40
+ * Record a change to accepted scope or behaviour.
41
+ *
42
+ * A summary and a reason are both required. A change with no reason is an event
43
+ * log entry wearing a different name, and the reason is the whole point: it is
44
+ * what tells the next reader whether the product drifted or was steered.
45
+ *
46
+ * Targets are required for the same reason. "Something changed" is not a record
47
+ * anyone can act on, so a change names what it moved.
48
+ */
49
+ export async function recordChange(root, input = {}) {
50
+ const summary = String(input.summary ?? "").trim();
51
+ const reason = String(input.reason ?? "").trim();
52
+ if (!summary) {
53
+ throw new ChangeError(E.INVALID, "A change needs a summary saying what moved.");
54
+ }
55
+ if (!reason) {
56
+ throw new ChangeError(E.INVALID,
57
+ "A change needs its reason. Without it nobody can tell later whether the product was steered or drifted.");
58
+ }
59
+
60
+ const changeType = String(input.changeType ?? "scope_changed");
61
+ if (!CHANGE_TYPES.includes(changeType)) {
62
+ throw new ChangeError(E.INVALID,
63
+ `"${changeType}" is not a kind of change. Use one of: ${CHANGE_TYPES.join(", ")}.`);
64
+ }
65
+
66
+ const targets = asList(input.targets);
67
+ if (targets.length === 0) {
68
+ throw new ChangeError(E.INVALID,
69
+ "A change names what it moved. Pass at least one target as <type>:<id>, for example feature:FEAT-0001.");
70
+ }
71
+
72
+ const parsed = targets.map((t) => {
73
+ const [type, id, ...rest] = t.split(":");
74
+ if (!type || !id) {
75
+ throw new ChangeError(E.INVALID,
76
+ `"${t}" is not a target. Write it as <type>:<id>, for example feature:FEAT-0001.`);
77
+ }
78
+ return { type, id, what: rest.join(":") || null };
79
+ });
80
+
81
+ return mutate(root, async (db) => {
82
+ const project = await db.get("SELECT id FROM projects LIMIT 1");
83
+ if (!project) throw new ChangeError(E.NOT_FOUND, "There is no project to record a change against.");
84
+
85
+ // A target that does not exist makes the audit trail point at nothing, and
86
+ // a dangling reference is the failure the record links validator exists to
87
+ // catch. Better to refuse than to write one.
88
+ for (const t of parsed) {
89
+ const table = TARGET_TABLES[t.type];
90
+ if (!table) {
91
+ throw new ChangeError(E.INVALID,
92
+ `"${t.type}" is not a kind of record this can point at. Known kinds: ${Object.keys(TARGET_TABLES).join(", ")}.`);
93
+ }
94
+ const found = await db.get(`SELECT 1 AS ok FROM ${table} WHERE id = ?`, t.id);
95
+ if (!found) throw new ChangeError(E.NOT_FOUND, `There is no ${t.type} ${t.id}.`);
96
+ }
97
+
98
+ const row = await create(db, "change", {
99
+ project_id: project.id,
100
+ summary: assertStorable("summary", summary),
101
+ reason: assertStorable("reason", reason),
102
+ change_type: changeType,
103
+ requested_by: input.requestedBy ?? null,
104
+ decided_by: input.decidedBy ?? input.actor ?? null,
105
+ decision_id: input.decisionId ?? null,
106
+ task_id: input.taskId ?? null,
107
+ session_id: input.sessionId ?? null,
108
+ status: "recorded",
109
+ }, {
110
+ projectId: project.id,
111
+ actor: input.actor ?? "superdev",
112
+ sessionId: input.sessionId ?? null,
113
+ taskId: input.taskId ?? null,
114
+ activityType: "scope_changed",
115
+ activitySummary: `Change recorded: ${summary}`.slice(0, 200),
116
+ });
117
+
118
+ for (const t of parsed) {
119
+ await db.run(
120
+ "INSERT OR IGNORE INTO change_targets (change_id, target_type, target_id, what_changed) VALUES (?, ?, ?, ?)",
121
+ row.id, t.type, t.id, t.what ? assertStorable("what_changed", t.what) : null,
122
+ );
123
+ }
124
+
125
+ row.targets = parsed;
126
+ return row;
127
+ });
128
+ }
129
+
130
+ /** Where each target type resolves, so a change cannot point at nothing. */
131
+ const TARGET_TABLES = {
132
+ goal: "goals",
133
+ milestone: "milestones",
134
+ module: "modules",
135
+ feature: "features",
136
+ workflow: "workflows",
137
+ workflow_step: "workflow_steps",
138
+ surface: "surfaces",
139
+ api_operation: "api_operations",
140
+ api_service: "api_services",
141
+ data_entity: "data_entities",
142
+ integration: "integrations",
143
+ task: "tasks",
144
+ decision: "decisions",
145
+ nfr: "non_functional_requirements",
146
+ acceptance_criterion: "feature_acceptance_criteria",
147
+ test_plan: "test_plans",
148
+ };
149
+
150
+ /** Every change, newest first, each carrying what it moved. */
151
+ export async function listChanges(root, { limit = 50, targetType = null, targetId = null } = {}) {
152
+ return query(root, async (db) => {
153
+ const filtered = targetType && targetId;
154
+ const rows = await db.all(
155
+ `SELECT c.* FROM changes c
156
+ ${filtered ? "JOIN change_targets t ON t.change_id = c.id AND t.target_type = ? AND t.target_id = ?" : ""}
157
+ ORDER BY c.created_at DESC, c.id DESC LIMIT ?`,
158
+ ...(filtered ? [targetType, targetId, limit] : [limit]),
159
+ );
160
+ for (const row of rows) {
161
+ row.targets = await db.all(
162
+ "SELECT target_type, target_id, what_changed FROM change_targets WHERE change_id = ?", row.id);
163
+ }
164
+ return rows;
165
+ });
166
+ }
167
+
168
+ /** One change in full. */
169
+ export async function showChange(root, changeId) {
170
+ return query(root, async (db) => {
171
+ const row = await db.get("SELECT * FROM changes WHERE id = ?", changeId);
172
+ if (!row) throw new ChangeError(E.NOT_FOUND, `There is no change ${changeId}.`);
173
+ row.targets = await db.all(
174
+ "SELECT target_type, target_id, what_changed FROM change_targets WHERE change_id = ?", changeId);
175
+ if (row.decision_id) {
176
+ row.decision = await db.get("SELECT id, title, status FROM decisions WHERE id = ?", row.decision_id);
177
+ }
178
+ return row;
179
+ });
180
+ }
@@ -0,0 +1,183 @@
1
+ // The agreed verification strategies, and whether anyone has run them.
2
+ //
3
+ // Section 9.3 makes the accepted test plan a completion condition. A plan is
4
+ // therefore not a document: it is a gate, and a gate needs a way to be opened.
5
+ // Two ways exist here, and the difference between them is the point.
6
+ //
7
+ // A plan whose how_to_run is a command this project can run unattended is run,
8
+ // and the result recorded is whatever the run produced. Nothing asserts that it
9
+ // passed. A plan whose how_to_run is a journey, such as opening every area of
10
+ // the control centre or restoring a backup and checking what came back, cannot
11
+ // be run by a script, and pretending otherwise would put a self issued pass
12
+ // where an observation belongs. Those are recorded by whoever performed them,
13
+ // and say who and what they saw.
14
+
15
+ import { create, mutate, query } from "../db/store.mjs";
16
+ import { refuseReason, runCheck } from "../verify/index.mjs";
17
+
18
+ export const E = {
19
+ NOT_FOUND: "E_NOT_FOUND",
20
+ NOT_ACCEPTED: "E_PLAN_NOT_ACCEPTED",
21
+ NOT_RUNNABLE: "E_PLAN_NOT_RUNNABLE",
22
+ };
23
+
24
+ export class PlanError extends Error {
25
+ constructor(code, message, detail) {
26
+ super(message);
27
+ this.name = "PlanError";
28
+ this.code = code;
29
+ if (detail !== undefined) this.detail = detail;
30
+ }
31
+ }
32
+
33
+ /** How many current passing runs a plan carries, as a subquery every read wants. */
34
+ const PASSING_RUNS = `(SELECT COUNT(*) FROM verification_evidence e
35
+ WHERE e.test_plan_id = p.id AND e.status = 'current' AND e.result = 'pass'
36
+ AND (e.last_check_result IS NULL OR e.last_check_result = 'pass'))`;
37
+
38
+ export async function listPlans(root) {
39
+ const rows = await query(root, (db) => db.all(
40
+ `SELECT p.id, p.name, p.strategy, p.how_to_run, p.passing_condition, p.status,
41
+ p.feature_id, p.module_id, p.workflow_id, ${PASSING_RUNS} AS passing_runs
42
+ FROM test_plans p ORDER BY p.id`));
43
+ return rows.map((p) => ({
44
+ ...p,
45
+ passing_runs: Number(p.passing_runs),
46
+ satisfied: Number(p.passing_runs) > 0,
47
+ // Whether this plan can be closed by a command or only by a person, said
48
+ // once here so no caller has to work it out again.
49
+ runnable: refuseReason(p.how_to_run) === null,
50
+ why_not_runnable: refuseReason(p.how_to_run),
51
+ }));
52
+ }
53
+
54
+ export async function showPlan(root, id) {
55
+ const found = await query(root, async (db) => ({
56
+ plan: await db.get(
57
+ `SELECT p.*, ${PASSING_RUNS} AS passing_runs FROM test_plans p WHERE p.id = ?`, id),
58
+ runs: await db.all(
59
+ `SELECT id, task_id, summary, result, reference, recorded_by, recorded_at,
60
+ last_check_result, last_checked_at
61
+ FROM verification_evidence WHERE test_plan_id = ? ORDER BY recorded_at DESC LIMIT 20`, id),
62
+ }));
63
+ if (!found.plan) {
64
+ throw new PlanError(E.NOT_FOUND, `There is no test plan ${id}. Run superdev test-plan list to see them.`);
65
+ }
66
+ const p = found.plan;
67
+ return {
68
+ ...found,
69
+ plan: {
70
+ ...p,
71
+ passing_runs: Number(p.passing_runs),
72
+ satisfied: Number(p.passing_runs) > 0,
73
+ runnable: refuseReason(p.how_to_run) === null,
74
+ why_not_runnable: refuseReason(p.how_to_run),
75
+ },
76
+ };
77
+ }
78
+
79
+ async function acceptedPlan(db, id) {
80
+ const plan = await db.get("SELECT * FROM test_plans WHERE id = ?", id);
81
+ if (!plan) throw new PlanError(E.NOT_FOUND, `There is no test plan ${id}. Run superdev test-plan list to see them.`);
82
+ if (plan.status !== "accepted") {
83
+ throw new PlanError(E.NOT_ACCEPTED,
84
+ `${id} is ${plan.status.replace(/_/g, " ")}, not accepted. A plan nobody has agreed to cannot satisfy anything.`);
85
+ }
86
+ return plan;
87
+ }
88
+
89
+ /**
90
+ * Write one run of a plan into the evidence table.
91
+ *
92
+ * A new run of the same plan supersedes the previous one rather than joining
93
+ * it. Runs of a plan are not separate claims that accumulate: they are the same
94
+ * claim, answered again. Left as they were, a plan that failed on Tuesday and
95
+ * passed on Wednesday would report both forever, and every reader would have to
96
+ * work out which one still counts.
97
+ *
98
+ * The superseded rows stay readable, so the failure is still part of the
99
+ * record. Nothing is deleted, and `test-plan show` lists them in order.
100
+ */
101
+ async function recordEvidence(db, plan, { summary, result, reference, checkCommand, taskId, actor, sessionId }) {
102
+ await db.run(
103
+ `UPDATE verification_evidence SET status = 'superseded'
104
+ WHERE test_plan_id = ? AND status = 'current'`,
105
+ plan.id,
106
+ );
107
+ return create(db, "verification_evidence", {
108
+ project_id: plan.project_id,
109
+ task_id: taskId,
110
+ feature_id: plan.feature_id,
111
+ test_plan_id: plan.id,
112
+ evidence_type: checkCommand ? "command_output" : "manual_check",
113
+ summary,
114
+ reference,
115
+ result,
116
+ check_command: checkCommand,
117
+ last_checked_at: checkCommand ? new Date().toISOString() : null,
118
+ last_check_result: checkCommand ? result : null,
119
+ recorded_by: actor,
120
+ recorded_at: new Date().toISOString(),
121
+ status: "current",
122
+ }, {
123
+ projectId: plan.project_id, actor, sessionId, taskId,
124
+ activityType: "verification_attached",
125
+ activitySummary: `${plan.id} ${result === "pass" ? "ran and passed" : result === "fail" ? "ran and failed" : "was inconclusive"}: ${summary}`.slice(0, 200),
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Run a plan's recorded command and record what it showed.
131
+ *
132
+ * The result comes from the exit status, so a failing run is recorded as
133
+ * failing. That is the only version of this worth having: one that recorded a
134
+ * pass regardless would turn the completion gate into a formality.
135
+ */
136
+ export async function runPlan(root, id, { actor = "superdev", sessionId = null, taskId = null, apply = false } = {}) {
137
+ const plan = await query(root, (db) => acceptedPlan(db, id));
138
+ const refused = refuseReason(plan.how_to_run);
139
+ if (refused) {
140
+ throw new PlanError(E.NOT_RUNNABLE,
141
+ `${id} cannot be run unattended: ${refused}. Perform it and record what you saw with superdev test-plan record ${id} --summary "..." --result pass.`,
142
+ { howToRun: plan.how_to_run, why: refused });
143
+ }
144
+ const outcome = await runCheck(root, plan.how_to_run, { timeoutMs: 300000 });
145
+ const result = outcome.result === "pass" ? "pass" : outcome.result === "fail" ? "fail" : "inconclusive";
146
+ const summary = `${plan.how_to_run} ${result === "pass" ? "passed" : result === "fail" ? "failed" : "could not be judged"}. ${outcome.detail}`.trim().slice(0, 400);
147
+
148
+ if (!apply) return { applied: false, plan, result, summary, detail: outcome.detail };
149
+
150
+ const evidence = await mutate(root, async (db) => {
151
+ const current = await acceptedPlan(db, id);
152
+ return recordEvidence(db, current, {
153
+ summary, result, reference: null, checkCommand: plan.how_to_run, taskId, actor, sessionId,
154
+ });
155
+ });
156
+ return { applied: true, plan, result, summary, detail: outcome.detail, evidence };
157
+ }
158
+
159
+ /**
160
+ * Record a run that a person or an agent performed, for a plan no script can
161
+ * run. It takes a summary of what was observed rather than a bare verdict,
162
+ * because a pass with nothing behind it is what evidence exists to prevent.
163
+ */
164
+ export async function recordPlanRun(root, id, {
165
+ summary, result = "pass", reference = null, taskId = null,
166
+ actor = "superdev", sessionId = null, apply = false,
167
+ } = {}) {
168
+ if (!summary) {
169
+ throw new PlanError(E.NOT_FOUND, "Say what was actually observed when the plan was performed, not only whether it passed.");
170
+ }
171
+ if (!["pass", "fail", "inconclusive"].includes(result)) {
172
+ throw new PlanError(E.NOT_FOUND, `A result is pass, fail or inconclusive, not ${result}.`);
173
+ }
174
+ const plan = await query(root, (db) => acceptedPlan(db, id));
175
+ if (!apply) return { applied: false, plan, result, summary };
176
+ const evidence = await mutate(root, async (db) => {
177
+ const current = await acceptedPlan(db, id);
178
+ return recordEvidence(db, current, {
179
+ summary, result, reference, checkCommand: null, taskId, actor, sessionId,
180
+ });
181
+ });
182
+ return { applied: true, plan, result, summary, evidence };
183
+ }