superdev-cli 0.3.0 → 0.4.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.
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Superdev - tell it what you want to build, and it keeps the product map, documentation, decisions and status in step while specialist providers do the specialist work.",
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.4.0",
|
|
9
9
|
"homepage": "https://github.com/superdev-ai/superdev"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "superdev",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Describe a product in ordinary language and get a living map of goals, features, workflows, tasks, integrations, UI surfaces, decisions and evidence - plus a self-contained visual dashboard. Routes specialist work to external providers and reports their readiness truthfully.",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.4.0",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Rahul Retnan"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superdev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Describe a product in ordinary language and get a living map of goals, features, workflows, tasks, integrations, UI surfaces, decisions and evidence, plus a self-contained visual dashboard. Routes specialist work to external providers and reports their readiness truthfully.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rahul Retnan"
|
|
@@ -16,6 +16,6 @@
|
|
|
16
16
|
"homepage": "https://github.com/superdev-ai/superdev",
|
|
17
17
|
"repository": "https://github.com/superdev-ai/superdev",
|
|
18
18
|
"requires": {
|
|
19
|
-
"cli": "0.
|
|
19
|
+
"cli": "0.4.0"
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superdev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Describe a product in ordinary language and get a living map of goals, features, workflows, tasks, integrations, UI surfaces, decisions and evidence, plus a self-contained visual dashboard.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rahul Retnan"
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
"homepage": "https://github.com/superdev-ai/superdev",
|
|
23
23
|
"repository": "https://github.com/superdev-ai/superdev",
|
|
24
24
|
"requires": {
|
|
25
|
-
"cli": "0.
|
|
25
|
+
"cli": "0.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -137,6 +137,7 @@ Working
|
|
|
137
137
|
test-plan run <id> Run the plan and record what it produced
|
|
138
138
|
test-plan record <id> Record a plan carried out by hand
|
|
139
139
|
verify Re-run the checks the recorded evidence stands on
|
|
140
|
+
evidence supersede <id> Retire a record that no longer applies, with the reason
|
|
140
141
|
task cancel <id> Stop work that should not continue, with a reason
|
|
141
142
|
task complete <id> Finish a task once its verification passes
|
|
142
143
|
task block <id> Record why a task cannot move
|
|
@@ -2034,9 +2035,18 @@ async function cmdTaskEvidence(ctx) {
|
|
|
2034
2035
|
}
|
|
2035
2036
|
const { attachEvidence } = await lifecycle();
|
|
2036
2037
|
const task = await attachEvidence(ctx.root, id, evidence);
|
|
2038
|
+
const also = (task.alsoProving ?? []).filter((e) => e.id !== task.evidence.id);
|
|
2037
2039
|
return {
|
|
2038
2040
|
data: { applied: true, task },
|
|
2039
|
-
text:
|
|
2041
|
+
text: R.stitch([
|
|
2042
|
+
`${task.evidence.id} recorded against ${task.id}: ${summary}`,
|
|
2043
|
+
// Two current records for one criterion is legitimate, and it is also how a
|
|
2044
|
+
// correction looks. Saying so is what stops the older one being forgotten
|
|
2045
|
+
// until verify starts failing on a command that has moved.
|
|
2046
|
+
also.length
|
|
2047
|
+
? R.wrap(`${countWord(also.length, "record")} already ${also.length === 1 ? "proves" : "prove"} ${evidence.acceptanceCriterionId}: ${also.map((e) => e.id).join(", ")}. If this replaces one rather than adding to it, retire it with superdev evidence supersede ${also[0].id} --reason "<why>" --apply.`)
|
|
2048
|
+
: null,
|
|
2049
|
+
]),
|
|
2040
2050
|
};
|
|
2041
2051
|
}
|
|
2042
2052
|
|
|
@@ -2207,6 +2217,38 @@ async function cmdTaskBlock(ctx) {
|
|
|
2207
2217
|
* duplicate is superseded and says which task replaced it, which is the same
|
|
2208
2218
|
* answer for the reader and a better one for the record.
|
|
2209
2219
|
*/
|
|
2220
|
+
/**
|
|
2221
|
+
* Retire one evidence record that no longer applies.
|
|
2222
|
+
*
|
|
2223
|
+
* `evidence` rather than `task evidence supersede`, because the resolver reads two
|
|
2224
|
+
* words and the third would be taken as a task identifier. The record is the
|
|
2225
|
+
* subject here anyway, not the task.
|
|
2226
|
+
*/
|
|
2227
|
+
async function cmdEvidenceSupersede(ctx) {
|
|
2228
|
+
const id = requireWord(ctx.words, 2, 'Say which record: superdev evidence supersede <EV-id> --reason "<why it no longer applies>".');
|
|
2229
|
+
const reason = String(requireFlag(ctx.flags, "reason", "Superseding evidence needs a reason, because the record already said this was observed."));
|
|
2230
|
+
if (!ctx.apply) {
|
|
2231
|
+
return planned({ id, reason }, "supersede it", R.stitch([
|
|
2232
|
+
R.wrap(`Would supersede ${id}: ${reason}`),
|
|
2233
|
+
R.wrap("The record and its reason stay in history. It leaves the verification tally, and any acceptance criterion resting on it falls back to whatever else is current, or to unmet."),
|
|
2234
|
+
]));
|
|
2235
|
+
}
|
|
2236
|
+
const { supersedeEvidence } = await lifecycle();
|
|
2237
|
+
const out = await supersedeEvidence(ctx.root, id, { reason, actor: ctx.actor });
|
|
2238
|
+
return {
|
|
2239
|
+
data: out,
|
|
2240
|
+
text: R.stitch([
|
|
2241
|
+
`${id} is superseded and no longer counted.`,
|
|
2242
|
+
out.command ? R.wrap(`Its command was: ${out.command}`) : null,
|
|
2243
|
+
out.criterion
|
|
2244
|
+
? R.wrap(out.criterion.restingOn
|
|
2245
|
+
? `${out.criterion.id} is still met, now resting on ${out.criterion.restingOn}.`
|
|
2246
|
+
: `${out.criterion.id} is ${R.status(out.criterion.status)}, because nothing current proves it now.`)
|
|
2247
|
+
: null,
|
|
2248
|
+
]),
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2210
2252
|
async function cmdTaskMerge(ctx) {
|
|
2211
2253
|
const duplicate = requireWord(ctx.words, 2, "Say which task is the duplicate: superdev task merge <duplicate-id> --into <TASK-id>.");
|
|
2212
2254
|
const survivor = String(requireFlag(ctx.flags, "into", "Say which task keeps the work: --into <TASK-id>."));
|
|
@@ -3164,6 +3206,7 @@ const COMMANDS = {
|
|
|
3164
3206
|
"task cancel": cmdTaskCancel,
|
|
3165
3207
|
"task complete": cmdTaskComplete,
|
|
3166
3208
|
"task block": cmdTaskBlock,
|
|
3209
|
+
"evidence supersede": cmdEvidenceSupersede,
|
|
3167
3210
|
"task merge": cmdTaskMerge,
|
|
3168
3211
|
"task reopen": cmdTaskReopen,
|
|
3169
3212
|
"docs generate": cmdDocsGenerate,
|
|
@@ -3240,7 +3283,7 @@ const COMMANDS = {
|
|
|
3240
3283
|
"category restore": cmdCategoryRestore,
|
|
3241
3284
|
};
|
|
3242
3285
|
|
|
3243
|
-
const GROUPS = new Set(["db", "task", "docs", "memory", "question", "decision", "category", "feature",
|
|
3286
|
+
const GROUPS = new Set(["db", "task", "docs", "memory", "question", "decision", "category", "feature", "evidence",
|
|
3244
3287
|
"module", "goal", "milestone", "workflow", "architecture", "schema", "api", "integration", "change", "assumption", "cloud"]);
|
|
3245
3288
|
|
|
3246
3289
|
function resolveCommand(words) {
|
package/src/runtime/hooks.mjs
CHANGED
|
@@ -491,10 +491,22 @@ async function postToolUse(root, payload) {
|
|
|
491
491
|
// A command that looked like a write and changed nothing is not work.
|
|
492
492
|
if (!changed.length) return EMPTY;
|
|
493
493
|
const moved = movedSince(root, changed, readTouched(root).attributed);
|
|
494
|
-
|
|
494
|
+
// A generated document that Superdev just rewrote is not the product changing.
|
|
495
|
+
//
|
|
496
|
+
// `docs generate` rewrites its own projection of its own database, so running it
|
|
497
|
+
// after finishing a task produced a marker saying the product had changed while
|
|
498
|
+
// no task was claimed. That is the same mistake as counting the runtime
|
|
499
|
+
// directory, and it blocked a release: doctor raised its only high-severity
|
|
500
|
+
// warning about Superdev regenerating its own files.
|
|
501
|
+
//
|
|
502
|
+
// Decided from the documents table rather than from a path prefix, because a
|
|
503
|
+
// file somebody wrote by hand under the same directory is not a generated
|
|
504
|
+
// document and should still count.
|
|
505
|
+
const product = await notGenerated(root, moved);
|
|
506
|
+
fresh = product.length > 0;
|
|
495
507
|
markTouched(root, changed);
|
|
496
508
|
attribute(root, moved);
|
|
497
|
-
file = file ?? changed[0];
|
|
509
|
+
file = product[0]?.[0] ?? file ?? changed[0];
|
|
498
510
|
}
|
|
499
511
|
markTouched(root, [file]);
|
|
500
512
|
await flushTouched(root, { force: false });
|
|
@@ -601,6 +613,26 @@ export function movedSince(root, candidates, attributed) {
|
|
|
601
613
|
return moved;
|
|
602
614
|
}
|
|
603
615
|
|
|
616
|
+
/**
|
|
617
|
+
* The paths among these that are not registered generated documents.
|
|
618
|
+
*
|
|
619
|
+
* Reads the documents table, which is the only authority on what Superdev wrote:
|
|
620
|
+
* a hand-authored file sitting in the same directory is not a generated document,
|
|
621
|
+
* and a hand edit to one that is becomes a proposal through its own flow rather
|
|
622
|
+
* than passing through here. A database that cannot be read leaves every path in,
|
|
623
|
+
* because failing to notice real work is the worse mistake.
|
|
624
|
+
*/
|
|
625
|
+
export async function notGenerated(root, moved) {
|
|
626
|
+
if (!moved.length) return moved;
|
|
627
|
+
try {
|
|
628
|
+
const rows = await query(root, (db) => db.all("SELECT path FROM documents"));
|
|
629
|
+
const generated = new Set(rows.map((r) => r.path));
|
|
630
|
+
return moved.filter(([path]) => !generated.has(path));
|
|
631
|
+
} catch {
|
|
632
|
+
return moved;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
604
636
|
/** Record that these paths are accounted for, at the state they are in now. */
|
|
605
637
|
function attribute(root, moved) {
|
|
606
638
|
if (!moved.length) return;
|
package/src/tasks/lifecycle.mjs
CHANGED
|
@@ -821,6 +821,21 @@ export async function attachEvidence(root, taskId, evidence = {}) {
|
|
|
821
821
|
|
|
822
822
|
return mutate(root, async (db) => {
|
|
823
823
|
const task = await taskOr404(db, taskId);
|
|
824
|
+
// What already proves this criterion, read before the new row is written.
|
|
825
|
+
//
|
|
826
|
+
// Two current records for one criterion is legitimate: two different checks can
|
|
827
|
+
// both prove one thing. It is also how a correction is made, and the second row
|
|
828
|
+
// silently joining the first is how a project ends up with one record failing
|
|
829
|
+
// forever because its command moved. So the caller is told, and told what to do
|
|
830
|
+
// about it, rather than the product guessing which of the two was meant.
|
|
831
|
+
const alreadyProving = acceptanceCriterionId
|
|
832
|
+
? await db.all(
|
|
833
|
+
`SELECT id, check_command FROM verification_evidence
|
|
834
|
+
WHERE acceptance_criterion_id = ? AND status = 'current' AND result = 'pass'
|
|
835
|
+
ORDER BY recorded_at, id`,
|
|
836
|
+
acceptanceCriterionId)
|
|
837
|
+
: [];
|
|
838
|
+
|
|
824
839
|
if (acceptanceCriterionId && result === "pass") {
|
|
825
840
|
// Fresh proof of a criterion retires the stale proof it replaces. Marking
|
|
826
841
|
// evidence stale is verify saying the check stopped passing, and the
|
|
@@ -906,6 +921,9 @@ export async function attachEvidence(root, taskId, evidence = {}) {
|
|
|
906
921
|
|
|
907
922
|
const task_after = await db.get("SELECT * FROM tasks WHERE id = ?", taskId);
|
|
908
923
|
task_after.evidence = row;
|
|
924
|
+
// Carried out so the surface can say it. A correction that looks identical to a
|
|
925
|
+
// second independent proof is how one record ends up failing forever.
|
|
926
|
+
task_after.alsoProving = alreadyProving;
|
|
909
927
|
return task_after;
|
|
910
928
|
});
|
|
911
929
|
}
|
|
@@ -960,3 +978,73 @@ export async function findTaskForWork(root, { description, featureId = null, lim
|
|
|
960
978
|
return best.row;
|
|
961
979
|
});
|
|
962
980
|
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Retire one evidence record that no longer applies.
|
|
984
|
+
*
|
|
985
|
+
* A check whose script moved keeps failing forever, and the row cannot be
|
|
986
|
+
* corrected: `task evidence` only appends. Under the old verify allowlist such a
|
|
987
|
+
* row was refused and merely counted as unrunnable, so it was invisible; once
|
|
988
|
+
* containment let the path through, verify ran it, the file was gone, and it
|
|
989
|
+
* failed on every run of a healthy project. `decision supersede` and
|
|
990
|
+
* `memory supersede` have existed all along; this is the same shape for the one
|
|
991
|
+
* record type that is the whole basis of a completion claim.
|
|
992
|
+
*
|
|
993
|
+
* Nothing is deleted. What was observed and when is the point of keeping it, and
|
|
994
|
+
* the reason it stopped applying is worth as much as the observation was.
|
|
995
|
+
*/
|
|
996
|
+
export async function supersedeEvidence(root, evidenceId, { reason, actor = "superdev" } = {}) {
|
|
997
|
+
if (!reason) {
|
|
998
|
+
throw new TaskError(E.REASON_REQUIRED,
|
|
999
|
+
"Superseding evidence needs a reason, because the record already said this was observed.");
|
|
1000
|
+
}
|
|
1001
|
+
return mutate(root, async (db) => {
|
|
1002
|
+
const row = await db.get("SELECT * FROM verification_evidence WHERE id = ?", evidenceId);
|
|
1003
|
+
if (!row) {
|
|
1004
|
+
throw new TaskError(E.NOT_FOUND,
|
|
1005
|
+
`There is no evidence ${evidenceId}. superdev verify --json names every record it ran, and task show lists what a task carries.`);
|
|
1006
|
+
}
|
|
1007
|
+
if (row.status === "superseded") {
|
|
1008
|
+
throw new TaskError(E.INVALID_TRANSITION, `${evidenceId} is already superseded.`);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
await db.run("UPDATE verification_evidence SET status = 'superseded' WHERE id = ?", evidenceId);
|
|
1012
|
+
|
|
1013
|
+
// The criterion this was the proof for now rests on whatever else is current,
|
|
1014
|
+
// and on nothing if there is nothing else. Leaving it met on retired proof
|
|
1015
|
+
// would be the same defect as a green check that never ran.
|
|
1016
|
+
let criterion = null;
|
|
1017
|
+
if (row.acceptance_criterion_id) {
|
|
1018
|
+
const replacement = await db.get(
|
|
1019
|
+
`SELECT id FROM verification_evidence
|
|
1020
|
+
WHERE acceptance_criterion_id = ? AND status = 'current' AND result = 'pass' AND id <> ?
|
|
1021
|
+
ORDER BY recorded_at DESC, id DESC LIMIT 1`,
|
|
1022
|
+
row.acceptance_criterion_id, evidenceId,
|
|
1023
|
+
);
|
|
1024
|
+
const before = await db.get(
|
|
1025
|
+
"SELECT * FROM feature_acceptance_criteria WHERE id = ?", row.acceptance_criterion_id);
|
|
1026
|
+
if (before && before.evidence_id === evidenceId) {
|
|
1027
|
+
await db.run(
|
|
1028
|
+
"UPDATE feature_acceptance_criteria SET status = ?, evidence_id = ? WHERE id = ?",
|
|
1029
|
+
replacement ? "met" : "unmet", replacement?.id ?? null, row.acceptance_criterion_id,
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
criterion = {
|
|
1033
|
+
id: row.acceptance_criterion_id,
|
|
1034
|
+
status: before?.evidence_id === evidenceId ? (replacement ? "met" : "unmet") : before?.status ?? null,
|
|
1035
|
+
restingOn: replacement?.id ?? null,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
await recordActivity(db, row.project_id, {
|
|
1040
|
+
type: "verification_attached",
|
|
1041
|
+
actor,
|
|
1042
|
+
taskId: row.task_id,
|
|
1043
|
+
featureId: row.feature_id,
|
|
1044
|
+
summary: `Evidence ${evidenceId} superseded: ${reason}`,
|
|
1045
|
+
metadata: { evidenceId, reason, criterion: row.acceptance_criterion_id ?? null },
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
return { evidenceId, was: row.status, summary: row.summary, command: row.check_command ?? null, criterion };
|
|
1049
|
+
});
|
|
1050
|
+
}
|