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,468 @@
|
|
|
1
|
+
// The product map commands: reading the model back out.
|
|
2
|
+
//
|
|
3
|
+
// Section 12.4 of the requirements document names fifteen of these and none
|
|
4
|
+
// existed. The records were all there, and the only way to see a module, a
|
|
5
|
+
// goal, a milestone, a workflow, the schema or the API surface was to open the
|
|
6
|
+
// control centre, which means a terminal session or a harness with no browser
|
|
7
|
+
// could write the model and never read it back.
|
|
8
|
+
//
|
|
9
|
+
// These are all read only. Every one answers a question the product promises an
|
|
10
|
+
// answer to in section 2.3, and each says plainly when the answer is nothing.
|
|
11
|
+
|
|
12
|
+
import { query } from "../db/store.mjs";
|
|
13
|
+
import * as R from "./render.mjs";
|
|
14
|
+
|
|
15
|
+
const json = (value, fallback) => {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(value ?? "null");
|
|
18
|
+
return parsed ?? fallback;
|
|
19
|
+
} catch { return fallback; }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const list = (values) => (values ?? []).filter(Boolean).join(", ") || null;
|
|
23
|
+
const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
24
|
+
|
|
25
|
+
/** Nothing recorded is an answer, and a useful one, so it is never blank. */
|
|
26
|
+
const nothing = (what) => R.wrap(`Nothing recorded yet for ${what}.`);
|
|
27
|
+
|
|
28
|
+
// ------------------------------------------------------------------ modules
|
|
29
|
+
|
|
30
|
+
export async function moduleList(root) {
|
|
31
|
+
const rows = await query(root, (db) => db.all(
|
|
32
|
+
`SELECT m.*, (SELECT COUNT(*) FROM features f WHERE f.module_id = m.id) AS features
|
|
33
|
+
FROM modules m ORDER BY m.sequence, m.id`));
|
|
34
|
+
if (!rows.length) return { data: { modules: [] }, text: nothing("modules") };
|
|
35
|
+
return {
|
|
36
|
+
data: { modules: rows },
|
|
37
|
+
text: R.stitch([
|
|
38
|
+
R.heading(`Modules (${rows.length})`),
|
|
39
|
+
R.table(["Id", "Name", "Features", "Status"],
|
|
40
|
+
rows.map((m) => [m.id, m.name, String(m.features), R.status(m.status)])),
|
|
41
|
+
]),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function moduleShow(root, id) {
|
|
46
|
+
const found = await query(root, async (db) => {
|
|
47
|
+
const module = await db.get("SELECT * FROM modules WHERE id = ?", id);
|
|
48
|
+
if (!module) return null;
|
|
49
|
+
return {
|
|
50
|
+
module,
|
|
51
|
+
features: await db.all("SELECT id, name, status FROM features WHERE module_id = ? ORDER BY id", id),
|
|
52
|
+
entities: await db.all("SELECT id, name FROM data_entities WHERE module_id = ? ORDER BY id", id),
|
|
53
|
+
operations: await db.all("SELECT id, name FROM api_operations WHERE module_id = ? ORDER BY id", id),
|
|
54
|
+
surfaces: await db.all("SELECT id, name FROM surfaces WHERE module_id = ? ORDER BY id", id),
|
|
55
|
+
integrations: await db.all("SELECT id, name FROM integrations WHERE module_id = ? ORDER BY id", id),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
if (!found) throw new Error(`There is no module ${id}.`);
|
|
59
|
+
const { module, features, entities, operations, surfaces, integrations } = found;
|
|
60
|
+
return {
|
|
61
|
+
data: found,
|
|
62
|
+
text: R.stitch([
|
|
63
|
+
R.heading(`${module.name} ${module.id}`),
|
|
64
|
+
R.pairs([
|
|
65
|
+
["Status", R.status(module.status)],
|
|
66
|
+
["Purpose", module.purpose ?? "Not recorded"],
|
|
67
|
+
["Outside its scope", module.out_of_scope ?? "Not recorded"],
|
|
68
|
+
]),
|
|
69
|
+
"",
|
|
70
|
+
R.block(`Features (${features.length})`, features.length
|
|
71
|
+
? R.bullets(features.map((f) => `${f.id} ${f.name} [${R.status(f.status)}]`))
|
|
72
|
+
: " None."),
|
|
73
|
+
entities.length ? R.block(`Data it owns (${entities.length})`, R.bullets(entities.map((e) => `${e.id} ${e.name}`))) : null,
|
|
74
|
+
operations.length ? R.block(`Operations (${operations.length})`, R.bullets(operations.map((o) => `${o.id} ${o.name}`))) : null,
|
|
75
|
+
surfaces.length ? R.block(`Surfaces (${surfaces.length})`, R.bullets(surfaces.map((s) => `${s.id} ${s.name}`))) : null,
|
|
76
|
+
integrations.length ? R.block(`Integrations (${integrations.length})`, R.bullets(integrations.map((i) => `${i.id} ${i.name}`))) : null,
|
|
77
|
+
]),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// -------------------------------------------------------------------- goals
|
|
82
|
+
|
|
83
|
+
export async function goalList(root) {
|
|
84
|
+
const rows = await query(root, (db) => db.all(
|
|
85
|
+
`SELECT g.*,
|
|
86
|
+
(SELECT COUNT(*) FROM feature_goals fg WHERE fg.goal_id = g.id) AS features,
|
|
87
|
+
(SELECT COUNT(*) FROM goal_success_criteria c WHERE c.goal_id = g.id) AS criteria,
|
|
88
|
+
(SELECT COUNT(*) FROM goal_success_criteria c WHERE c.goal_id = g.id AND c.status = 'met') AS met
|
|
89
|
+
FROM goals g ORDER BY g.sequence, g.id`));
|
|
90
|
+
if (!rows.length) return { data: { goals: [] }, text: nothing("goals") };
|
|
91
|
+
return {
|
|
92
|
+
data: { goals: rows },
|
|
93
|
+
text: R.stitch([
|
|
94
|
+
R.heading(`Goals (${rows.length})`),
|
|
95
|
+
R.table(["Id", "Name", "Features", "Criteria met"],
|
|
96
|
+
rows.map((g) => [g.id, g.name, String(g.features), `${g.met} of ${g.criteria}`])),
|
|
97
|
+
]),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function goalShow(root, id) {
|
|
102
|
+
const found = await query(root, async (db) => {
|
|
103
|
+
const goal = await db.get("SELECT * FROM goals WHERE id = ?", id);
|
|
104
|
+
if (!goal) return null;
|
|
105
|
+
return {
|
|
106
|
+
goal,
|
|
107
|
+
criteria: await db.all("SELECT * FROM goal_success_criteria WHERE goal_id = ? ORDER BY sequence", id),
|
|
108
|
+
features: await db.all(
|
|
109
|
+
`SELECT f.id, f.name, f.status FROM features f
|
|
110
|
+
JOIN feature_goals fg ON fg.feature_id = f.id WHERE fg.goal_id = ? ORDER BY f.id`, id),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
if (!found) throw new Error(`There is no goal ${id}.`);
|
|
114
|
+
const { goal, criteria, features } = found;
|
|
115
|
+
return {
|
|
116
|
+
data: found,
|
|
117
|
+
text: R.stitch([
|
|
118
|
+
R.heading(`${goal.name} ${goal.id}`),
|
|
119
|
+
R.pairs([
|
|
120
|
+
["Status", R.status(goal.status)],
|
|
121
|
+
["Outcome", goal.description ?? "Not recorded"],
|
|
122
|
+
["Why it matters", goal.why_it_matters ?? "Not recorded"],
|
|
123
|
+
]),
|
|
124
|
+
"",
|
|
125
|
+
R.block(`How it is measured (${criteria.length})`, criteria.length
|
|
126
|
+
? R.bullets(criteria.map((c) =>
|
|
127
|
+
`[${R.status(c.status)}] ${c.criterion}${c.target ? `. Target: ${c.target}` : ""}${c.measurement_method ? `. Read by: ${c.measurement_method}` : ""}`))
|
|
128
|
+
: " Nothing measurable is recorded, so nothing can show whether this goal was reached."),
|
|
129
|
+
R.block(`Features serving it (${features.length})`, features.length
|
|
130
|
+
? R.bullets(features.map((f) => `${f.id} ${f.name} [${R.status(f.status)}]`))
|
|
131
|
+
: " None, so nothing being built moves this goal."),
|
|
132
|
+
]),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --------------------------------------------------------------- milestones
|
|
137
|
+
|
|
138
|
+
export async function milestoneList(root) {
|
|
139
|
+
const rows = await query(root, (db) => db.all(
|
|
140
|
+
`SELECT m.*, (SELECT COUNT(*) FROM features f WHERE f.milestone_id = m.id) AS features
|
|
141
|
+
FROM milestones m ORDER BY m.sequence, m.id`));
|
|
142
|
+
if (!rows.length) return { data: { milestones: [] }, text: nothing("milestones") };
|
|
143
|
+
return {
|
|
144
|
+
data: { milestones: rows },
|
|
145
|
+
text: R.stitch([
|
|
146
|
+
R.heading(`Milestones (${rows.length})`),
|
|
147
|
+
R.table(["Id", "Name", "Features", "Status"],
|
|
148
|
+
rows.map((m) => [m.id, m.name, String(m.features), R.status(m.status)])),
|
|
149
|
+
]),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function milestoneShow(root, id) {
|
|
154
|
+
const found = await query(root, async (db) => {
|
|
155
|
+
const milestone = await db.get("SELECT * FROM milestones WHERE id = ?", id);
|
|
156
|
+
if (!milestone) return null;
|
|
157
|
+
return {
|
|
158
|
+
milestone,
|
|
159
|
+
features: await db.all("SELECT id, name, status FROM features WHERE milestone_id = ? ORDER BY id", id),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
if (!found) throw new Error(`There is no milestone ${id}.`);
|
|
163
|
+
const { milestone, features } = found;
|
|
164
|
+
// Both shapes are read: a plain string from before conditions could be
|
|
165
|
+
// judged, and the object a judged condition is stored as now.
|
|
166
|
+
const exits = json(milestone.exit_conditions_json, []).map((c) =>
|
|
167
|
+
typeof c === "string" ? { condition: c, met: false, reading: null, check: null } : c);
|
|
168
|
+
const met = exits.filter((c) => c.met).length;
|
|
169
|
+
const done = features.filter((f) => ["complete", "delivered", "implemented"].includes(f.status)).length;
|
|
170
|
+
return {
|
|
171
|
+
data: found,
|
|
172
|
+
text: R.stitch([
|
|
173
|
+
R.heading(`${milestone.name} ${milestone.id}`),
|
|
174
|
+
R.pairs([
|
|
175
|
+
["Status", R.status(milestone.status)],
|
|
176
|
+
["Delivers", milestone.outcome ?? "Not recorded"],
|
|
177
|
+
["Scheduled features", `${done} of ${features.length} complete`],
|
|
178
|
+
]),
|
|
179
|
+
"",
|
|
180
|
+
// A condition is stored as an object carrying its verdict and the reading
|
|
181
|
+
// that decided it, and was being printed straight into a bullet, which
|
|
182
|
+
// rendered twenty five of them as [object Object]. The control centre
|
|
183
|
+
// normalises the same shape; the command line was never taught to.
|
|
184
|
+
R.block(`Exit conditions (${met} of ${exits.length} met)`, exits.length
|
|
185
|
+
? exits.map((c) => R.stitch([
|
|
186
|
+
` [${c.met ? "met" : "not met"}] ${c.condition}`,
|
|
187
|
+
c.reading ? R.wrap(c.reading, R.WIDTH, " ") : null,
|
|
188
|
+
c.check ? ` Decided again by: ${c.check}` : null,
|
|
189
|
+
])).join("\n")
|
|
190
|
+
: " None recorded, so nothing says when this is reached."),
|
|
191
|
+
R.block(`Scheduled features (${features.length})`, features.length
|
|
192
|
+
? R.bullets(features.map((f) => `${f.id} ${f.name} [${R.status(f.status)}]`))
|
|
193
|
+
: " Nothing is scheduled into it."),
|
|
194
|
+
]),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ----------------------------------------------------------------- features
|
|
199
|
+
|
|
200
|
+
export async function featureList(root, { module: moduleId = null, status = null } = {}) {
|
|
201
|
+
const where = [];
|
|
202
|
+
const params = [];
|
|
203
|
+
if (moduleId) { where.push("f.module_id = ?"); params.push(moduleId); }
|
|
204
|
+
if (status) { where.push("f.status = ?"); params.push(status); }
|
|
205
|
+
const rows = await query(root, (db) => db.all(
|
|
206
|
+
`SELECT f.*, m.name AS module,
|
|
207
|
+
(SELECT COUNT(*) FROM feature_acceptance_criteria c WHERE c.feature_id = f.id) AS criteria,
|
|
208
|
+
(SELECT COUNT(*) FROM feature_acceptance_criteria c WHERE c.feature_id = f.id AND c.status = 'met') AS met
|
|
209
|
+
FROM features f LEFT JOIN modules m ON m.id = f.module_id
|
|
210
|
+
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
211
|
+
ORDER BY f.id`, ...params));
|
|
212
|
+
if (!rows.length) return { data: { features: [] }, text: nothing("features") };
|
|
213
|
+
return {
|
|
214
|
+
data: { features: rows },
|
|
215
|
+
text: R.stitch([
|
|
216
|
+
R.heading(`Features (${rows.length})`),
|
|
217
|
+
R.table(["Id", "Name", "Module", "Depth", "Criteria met", "Status"],
|
|
218
|
+
rows.map((f) => [f.id, f.name, f.module ?? "", f.spec_depth ?? "", `${f.met} of ${f.criteria}`, R.status(f.status)])),
|
|
219
|
+
]),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function featureShow(root, id) {
|
|
224
|
+
const found = await query(root, async (db) => {
|
|
225
|
+
const feature = await db.get("SELECT * FROM features WHERE id = ?", id);
|
|
226
|
+
if (!feature) return null;
|
|
227
|
+
return {
|
|
228
|
+
feature,
|
|
229
|
+
module: feature.module_id ? await db.get("SELECT id, name FROM modules WHERE id = ?", feature.module_id) : null,
|
|
230
|
+
milestone: feature.milestone_id ? await db.get("SELECT id, name FROM milestones WHERE id = ?", feature.milestone_id) : null,
|
|
231
|
+
goals: await db.all(
|
|
232
|
+
`SELECT g.id, g.name FROM goals g JOIN feature_goals fg ON fg.goal_id = g.id WHERE fg.feature_id = ?`, id),
|
|
233
|
+
criteria: await db.all("SELECT * FROM feature_acceptance_criteria WHERE feature_id = ? ORDER BY sequence", id),
|
|
234
|
+
workflows: await db.all("SELECT id, name, status FROM workflows WHERE feature_id = ? ORDER BY id", id),
|
|
235
|
+
tasks: await db.all("SELECT id, name, status FROM tasks WHERE feature_id = ? ORDER BY id", id),
|
|
236
|
+
plans: await db.all("SELECT id, name, status FROM test_plans WHERE feature_id = ? ORDER BY id", id),
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
if (!found) throw new Error(`There is no feature ${id}.`);
|
|
240
|
+
const { feature, module, milestone, goals, criteria, workflows, tasks, plans } = found;
|
|
241
|
+
return {
|
|
242
|
+
data: found,
|
|
243
|
+
text: R.stitch([
|
|
244
|
+
R.heading(`${feature.name} ${feature.id}`),
|
|
245
|
+
R.pairs([
|
|
246
|
+
["Status", R.status(feature.status)],
|
|
247
|
+
["Declared depth", feature.spec_depth ?? "not set"],
|
|
248
|
+
["Module", module ? `${module.name} (${module.id})` : "None"],
|
|
249
|
+
["Milestone", milestone ? `${milestone.name} (${milestone.id})` : "Not scheduled"],
|
|
250
|
+
["Goals served", list(goals.map((g) => g.id)) ?? "None"],
|
|
251
|
+
["Purpose", feature.purpose ?? "Not recorded"],
|
|
252
|
+
["Who wants it", feature.user_statement ?? "Not recorded"],
|
|
253
|
+
]),
|
|
254
|
+
"",
|
|
255
|
+
R.block(`Acceptance criteria (${criteria.length})`, criteria.length
|
|
256
|
+
? R.bullets(criteria.map((c) => `[${R.status(c.status)}] ${c.criterion}`))
|
|
257
|
+
: " None, so nothing says what done means."),
|
|
258
|
+
workflows.length ? R.block(`Workflows (${workflows.length})`, R.bullets(workflows.map((w) => `${w.id} ${w.name}`))) : null,
|
|
259
|
+
plans.length ? R.block(`Test plans (${plans.length})`, R.bullets(plans.map((p) => `${p.id} ${p.name} [${R.status(p.status)}]`))) : null,
|
|
260
|
+
R.block(`Tasks (${tasks.length})`, tasks.length
|
|
261
|
+
? R.bullets(tasks.map((t) => `${t.id} ${t.name} [${R.status(t.status)}]`))
|
|
262
|
+
: " None derived yet."),
|
|
263
|
+
]),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------- workflows
|
|
268
|
+
|
|
269
|
+
export async function workflowList(root) {
|
|
270
|
+
const rows = await query(root, (db) => db.all(
|
|
271
|
+
`SELECT w.*, f.name AS feature,
|
|
272
|
+
(SELECT COUNT(*) FROM workflow_steps s WHERE s.workflow_id = w.id) AS steps
|
|
273
|
+
FROM workflows w LEFT JOIN features f ON f.id = w.feature_id ORDER BY w.id`));
|
|
274
|
+
if (!rows.length) return { data: { workflows: [] }, text: nothing("workflows") };
|
|
275
|
+
return {
|
|
276
|
+
data: { workflows: rows },
|
|
277
|
+
text: R.stitch([
|
|
278
|
+
R.heading(`Workflows (${rows.length})`),
|
|
279
|
+
R.table(["Id", "Name", "Steps", "Status"],
|
|
280
|
+
rows.map((w) => [w.id, w.name, String(w.steps), R.status(w.status)])),
|
|
281
|
+
]),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function workflowShow(root, id) {
|
|
286
|
+
const found = await query(root, async (db) => {
|
|
287
|
+
const workflow = await db.get("SELECT * FROM workflows WHERE id = ?", id);
|
|
288
|
+
if (!workflow) return null;
|
|
289
|
+
return {
|
|
290
|
+
workflow,
|
|
291
|
+
feature: workflow.feature_id ? await db.get("SELECT id, name FROM features WHERE id = ?", workflow.feature_id) : null,
|
|
292
|
+
steps: await db.all("SELECT * FROM workflow_steps WHERE workflow_id = ? ORDER BY sequence", id),
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
if (!found) throw new Error(`There is no workflow ${id}.`);
|
|
296
|
+
const { workflow, feature, steps } = found;
|
|
297
|
+
return {
|
|
298
|
+
data: found,
|
|
299
|
+
text: R.stitch([
|
|
300
|
+
R.heading(`${workflow.name} ${workflow.id}`),
|
|
301
|
+
R.pairs([
|
|
302
|
+
["Status", R.status(workflow.status)],
|
|
303
|
+
["Feature", feature ? `${feature.name} (${feature.id})` : "None"],
|
|
304
|
+
["Trigger", workflow.trigger ?? "Not recorded"],
|
|
305
|
+
["Complete when", workflow.completion_criteria ?? "Not recorded"],
|
|
306
|
+
]),
|
|
307
|
+
"",
|
|
308
|
+
R.heading(`Steps (${steps.length})`),
|
|
309
|
+
steps.length
|
|
310
|
+
? steps.map((s) => R.stitch([
|
|
311
|
+
`${s.sequence}. ${s.action}`,
|
|
312
|
+
R.wrap(`Expected: ${s.expected_result ?? "not recorded"}`, R.WIDTH, " "),
|
|
313
|
+
s.failure_behavior ? R.wrap(`On failure: ${s.failure_behavior}`, R.WIDTH, " ") : null,
|
|
314
|
+
])).join("\n")
|
|
315
|
+
: R.wrap("No steps recorded, so following this workflow proves nothing."),
|
|
316
|
+
]),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ------------------------------------------------------------- architecture
|
|
321
|
+
|
|
322
|
+
export async function architectureShow(root) {
|
|
323
|
+
const a = await query(root, async (db) => ({
|
|
324
|
+
pieces: await db.all("SELECT * FROM runtime_pieces ORDER BY sequence, id"),
|
|
325
|
+
edges: await db.all("SELECT * FROM runtime_piece_edges"),
|
|
326
|
+
modules: await db.all("SELECT id, name, status FROM modules ORDER BY sequence, id"),
|
|
327
|
+
integrations: await db.all("SELECT id, name, purpose, configuration_status FROM integrations ORDER BY id"),
|
|
328
|
+
}));
|
|
329
|
+
const nameOf = new Map(a.pieces.map((p) => [p.id, p.name]));
|
|
330
|
+
return {
|
|
331
|
+
data: a,
|
|
332
|
+
text: R.stitch([
|
|
333
|
+
R.heading(`Runtime pieces (${a.pieces.length})`),
|
|
334
|
+
a.pieces.length
|
|
335
|
+
? R.table(["Name", "Runs where", "Shown by"],
|
|
336
|
+
a.pieces.map((p) => [p.name, p.runs_where ?? "", p.evidence_ref ?? ""]))
|
|
337
|
+
: R.wrap("Nothing recorded, so the architecture is undescribed."),
|
|
338
|
+
"",
|
|
339
|
+
R.block(`Connections (${a.edges.length})`, a.edges.length
|
|
340
|
+
? R.bullets(a.edges.map((e) => `${nameOf.get(e.from_piece_id) ?? e.from_piece_id} to ${nameOf.get(e.to_piece_id) ?? e.to_piece_id}: ${e.protocol ?? "unspecified"}`))
|
|
341
|
+
: " None recorded."),
|
|
342
|
+
R.block(`Modules (${a.modules.length})`, R.bullets(a.modules.map((m) => `${m.id} ${m.name}`))),
|
|
343
|
+
R.block(`Integrations (${a.integrations.length})`, a.integrations.length
|
|
344
|
+
? R.bullets(a.integrations.map((i) => `${i.name} [${R.status(i.configuration_status)}]: ${i.purpose ?? ""}`))
|
|
345
|
+
: " None recorded."),
|
|
346
|
+
]),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ------------------------------------------------------------------- schema
|
|
351
|
+
|
|
352
|
+
export async function schemaShow(root, { entity = null } = {}) {
|
|
353
|
+
if (entity) {
|
|
354
|
+
const found = await query(root, async (db) => {
|
|
355
|
+
const e = await db.get("SELECT * FROM data_entities WHERE id = ? OR name = ?", entity, entity);
|
|
356
|
+
if (!e) return null;
|
|
357
|
+
return {
|
|
358
|
+
entity: e,
|
|
359
|
+
fields: await db.all("SELECT * FROM data_fields WHERE entity_id = ? ORDER BY sequence", e.id),
|
|
360
|
+
out: await db.all(
|
|
361
|
+
`SELECT r.*, t.name AS target FROM data_relationships r
|
|
362
|
+
JOIN data_entities t ON t.id = r.to_entity_id WHERE r.from_entity_id = ?`, e.id),
|
|
363
|
+
in: await db.all(
|
|
364
|
+
`SELECT r.*, s.name AS source FROM data_relationships r
|
|
365
|
+
JOIN data_entities s ON s.id = r.from_entity_id WHERE r.to_entity_id = ?`, e.id),
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
if (!found) throw new Error(`There is no data entity ${entity}.`);
|
|
369
|
+
return {
|
|
370
|
+
data: found,
|
|
371
|
+
text: R.stitch([
|
|
372
|
+
R.heading(`${found.entity.name} ${found.entity.id}`),
|
|
373
|
+
R.pairs([
|
|
374
|
+
["Status", R.status(found.entity.status)],
|
|
375
|
+
["Purpose", found.entity.purpose ?? "Not recorded"],
|
|
376
|
+
["Stored in", found.entity.store ?? "Nowhere yet"],
|
|
377
|
+
]),
|
|
378
|
+
"",
|
|
379
|
+
found.fields.length
|
|
380
|
+
? R.table(["Field", "Type", "Nullable", "Sensitivity"],
|
|
381
|
+
found.fields.map((f) => [f.name, f.type, f.nullable ? "yes" : "no", f.sensitivity_class]))
|
|
382
|
+
: R.wrap(found.entity.status === "planned"
|
|
383
|
+
? "No table exists for this yet, so it has no fields."
|
|
384
|
+
: "No fields recorded."),
|
|
385
|
+
found.out.length ? R.block("Points at", R.bullets(found.out.map((r) => `${r.name} to ${r.target} (${r.cardinality})`))) : null,
|
|
386
|
+
found.in.length ? R.block("Pointed at by", R.bullets(found.in.map((r) => `${r.source}.${r.name} (${r.cardinality})`))) : null,
|
|
387
|
+
]),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const s = await query(root, async (db) => ({
|
|
392
|
+
entities: await db.all(
|
|
393
|
+
`SELECT e.*, (SELECT COUNT(*) FROM data_fields f WHERE f.entity_id = e.id) AS fields
|
|
394
|
+
FROM data_entities e ORDER BY e.name`),
|
|
395
|
+
relationships: (await db.get("SELECT COUNT(*) n FROM data_relationships")).n,
|
|
396
|
+
migrations: await db.all("SELECT version, name FROM applied_migrations ORDER BY version"),
|
|
397
|
+
}));
|
|
398
|
+
return {
|
|
399
|
+
data: s,
|
|
400
|
+
text: R.stitch([
|
|
401
|
+
R.heading(`Data entities (${s.entities.length})`),
|
|
402
|
+
s.entities.length
|
|
403
|
+
? R.table(["Id", "Name", "Fields", "Status"],
|
|
404
|
+
s.entities.map((e) => [e.id, e.name, String(e.fields), R.status(e.status)]))
|
|
405
|
+
: R.wrap("Nothing recorded."),
|
|
406
|
+
"",
|
|
407
|
+
R.pairs([
|
|
408
|
+
["Relationships", String(s.relationships)],
|
|
409
|
+
["Migrations applied", String(s.migrations.length)],
|
|
410
|
+
["Schema version", String(s.migrations.at(-1)?.version ?? 0)],
|
|
411
|
+
]),
|
|
412
|
+
R.wrap("Pass an entity name or id to see its fields and relationships."),
|
|
413
|
+
]),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------- api
|
|
418
|
+
|
|
419
|
+
export async function apiShow(root) {
|
|
420
|
+
const a = await query(root, async (db) => ({
|
|
421
|
+
services: await db.all("SELECT * FROM api_services ORDER BY sequence, id"),
|
|
422
|
+
operations: await db.all(
|
|
423
|
+
`SELECT o.*, m.name AS module FROM api_operations o
|
|
424
|
+
LEFT JOIN modules m ON m.id = o.module_id ORDER BY o.name`),
|
|
425
|
+
}));
|
|
426
|
+
const byService = new Map();
|
|
427
|
+
for (const op of a.operations) {
|
|
428
|
+
const key = op.api_service_id ?? "(no service)";
|
|
429
|
+
if (!byService.has(key)) byService.set(key, []);
|
|
430
|
+
byService.get(key).push(op);
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
data: a,
|
|
434
|
+
text: R.stitch([
|
|
435
|
+
R.heading(`API operations (${a.operations.length})`),
|
|
436
|
+
a.operations.length
|
|
437
|
+
? R.table(["Name", "Style", "Changes state", "Status"],
|
|
438
|
+
a.operations.slice(0, 60).map((o) => [
|
|
439
|
+
o.name, o.style, o.side_effects_json && o.side_effects_json !== "[]" ? "yes" : "no", R.status(o.status)]))
|
|
440
|
+
: R.wrap("Nothing recorded."),
|
|
441
|
+
a.operations.length > 60 ? R.wrap(`Showing the first 60 of ${a.operations.length}. Use --json for all of them.`) : null,
|
|
442
|
+
"",
|
|
443
|
+
R.block(`Services (${a.services.length})`, a.services.length
|
|
444
|
+
? R.bullets(a.services.map((s) => `${s.id} ${s.name} ${count(byService.get(s.id)?.length ?? 0, "operation")}`))
|
|
445
|
+
: " None recorded, so every operation is loose rather than grouped under a boundary."),
|
|
446
|
+
]),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// -------------------------------------------------------------- integrations
|
|
451
|
+
|
|
452
|
+
export async function integrationList(root) {
|
|
453
|
+
const rows = await query(root, (db) => db.all(
|
|
454
|
+
`SELECT i.*, m.name AS module FROM integrations i
|
|
455
|
+
LEFT JOIN modules m ON m.id = i.module_id ORDER BY i.name`));
|
|
456
|
+
if (!rows.length) return { data: { integrations: [] }, text: nothing("integrations") };
|
|
457
|
+
return {
|
|
458
|
+
data: { integrations: rows },
|
|
459
|
+
text: R.stitch([
|
|
460
|
+
R.heading(`Integrations (${rows.length})`),
|
|
461
|
+
R.table(["Id", "Name", "Configured", "Verified"],
|
|
462
|
+
rows.map((i) => [i.id, i.name, R.status(i.configuration_status), R.status(i.verification_status)])),
|
|
463
|
+
"",
|
|
464
|
+
R.block("What happens when one is absent", R.bullets(
|
|
465
|
+
rows.filter((i) => i.failure_behavior).slice(0, 8).map((i) => `${i.name}: ${i.failure_behavior}`))),
|
|
466
|
+
]),
|
|
467
|
+
};
|
|
468
|
+
}
|