@wrongstack/sdd 1.0.1 → 1.0.3
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/dist/index.js +44 -24
- package/dist/spec-store.d.ts +8 -0
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -521,7 +521,7 @@ function assertTaskGraphExecutionIntegrity(graph) {
|
|
|
521
521
|
const indegree = new Map(Array.from(graph.nodes.keys(), (id) => [id, 0]));
|
|
522
522
|
const outgoing = /* @__PURE__ */ new Map();
|
|
523
523
|
for (const edge of dependencies) {
|
|
524
|
-
indegree.set(edge.to,
|
|
524
|
+
indegree.set(edge.to, indegree.get(edge.to) + 1);
|
|
525
525
|
outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
|
|
526
526
|
}
|
|
527
527
|
const ready = Array.from(indegree.entries()).filter(([, degree]) => degree === 0).map(([id]) => id);
|
|
@@ -530,7 +530,7 @@ function assertTaskGraphExecutionIntegrity(graph) {
|
|
|
530
530
|
const id = ready.shift();
|
|
531
531
|
visited += 1;
|
|
532
532
|
for (const dependent of outgoing.get(id) ?? []) {
|
|
533
|
-
const next =
|
|
533
|
+
const next = indegree.get(dependent) - 1;
|
|
534
534
|
indegree.set(dependent, next);
|
|
535
535
|
if (next === 0) ready.push(dependent);
|
|
536
536
|
}
|
|
@@ -757,8 +757,9 @@ var SpecStore = class {
|
|
|
757
757
|
await this.updateIndex(spec);
|
|
758
758
|
}
|
|
759
759
|
async load(id) {
|
|
760
|
+
const filePath = this.filePath(id);
|
|
760
761
|
try {
|
|
761
|
-
const raw = await fsp.readFile(
|
|
762
|
+
const raw = await fsp.readFile(filePath, "utf8");
|
|
762
763
|
return JSON.parse(raw);
|
|
763
764
|
} catch {
|
|
764
765
|
return null;
|
|
@@ -769,8 +770,9 @@ var SpecStore = class {
|
|
|
769
770
|
return index.entries.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
770
771
|
}
|
|
771
772
|
async delete(id) {
|
|
773
|
+
const filePath = this.filePath(id);
|
|
772
774
|
try {
|
|
773
|
-
await fsp.unlink(
|
|
775
|
+
await fsp.unlink(filePath);
|
|
774
776
|
await this.removeFromIndex(id);
|
|
775
777
|
return true;
|
|
776
778
|
} catch {
|
|
@@ -816,8 +818,25 @@ var SpecStore = class {
|
|
|
816
818
|
await this.save(updated);
|
|
817
819
|
return updated;
|
|
818
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Resolve a spec id to its file, refusing anything that escapes `baseDir`.
|
|
823
|
+
* `id` arrives from a WebSocket frame (`specs-ws-handler.ts:96-97,152`,
|
|
824
|
+
* raw `as string` casts) and from any persisted spec JSON. A bare
|
|
825
|
+
* `path.join` was the traversal primitive: `id = "../../../secret"`
|
|
826
|
+
* resolved outside the store and `load()` returned its contents. Mirrors
|
|
827
|
+
* the same containment `task-graph-store.ts:134-145` applies to its ids.
|
|
828
|
+
*/
|
|
819
829
|
filePath(id) {
|
|
820
|
-
|
|
830
|
+
if (typeof id !== "string" || id.length === 0 || id.length > 200 || /[\0/\\]/.test(id)) {
|
|
831
|
+
throw new Error(`Invalid spec id: ${JSON.stringify(id)}`);
|
|
832
|
+
}
|
|
833
|
+
const dir = path.resolve(this.baseDir);
|
|
834
|
+
const resolved = path.resolve(dir, `${id}.json`);
|
|
835
|
+
const rel = path.relative(dir, resolved);
|
|
836
|
+
if (rel.startsWith("..") || path.isAbsolute(rel) || rel.includes(path.sep)) {
|
|
837
|
+
throw new Error(`Invalid spec id: ${JSON.stringify(id)}`);
|
|
838
|
+
}
|
|
839
|
+
return resolved;
|
|
821
840
|
}
|
|
822
841
|
async readIndex() {
|
|
823
842
|
try {
|
|
@@ -3328,7 +3347,7 @@ var SddParallelRun = class {
|
|
|
3328
3347
|
const sessionId = this.currentSessionId();
|
|
3329
3348
|
this.events?.emit(
|
|
3330
3349
|
event,
|
|
3331
|
-
|
|
3350
|
+
{ ...payload, sessionId }
|
|
3332
3351
|
);
|
|
3333
3352
|
}
|
|
3334
3353
|
currentSessionId() {
|
|
@@ -3342,7 +3361,7 @@ var SddParallelRun = class {
|
|
|
3342
3361
|
/** Resolvers for tasks parked in `waitWhilePaused`, woken on resume/stop. */
|
|
3343
3362
|
pausedWaiters = /* @__PURE__ */ new Set();
|
|
3344
3363
|
notifyPausedWaiters() {
|
|
3345
|
-
for (const
|
|
3364
|
+
for (const resolve3 of this.pausedWaiters) resolve3();
|
|
3346
3365
|
this.pausedWaiters.clear();
|
|
3347
3366
|
}
|
|
3348
3367
|
/** Trigger stop — causes run() to abort after the current wave. */
|
|
@@ -3515,10 +3534,10 @@ var SddParallelRun = class {
|
|
|
3515
3534
|
}
|
|
3516
3535
|
async waitWhilePaused() {
|
|
3517
3536
|
while (this.paused && !this.stopRequested) {
|
|
3518
|
-
await new Promise((
|
|
3519
|
-
this.pausedWaiters.add(
|
|
3537
|
+
await new Promise((resolve3) => {
|
|
3538
|
+
this.pausedWaiters.add(resolve3);
|
|
3520
3539
|
const safety = setTimeout(() => {
|
|
3521
|
-
if (this.pausedWaiters.delete(
|
|
3540
|
+
if (this.pausedWaiters.delete(resolve3)) resolve3();
|
|
3522
3541
|
}, 1e3);
|
|
3523
3542
|
safety.unref?.();
|
|
3524
3543
|
});
|
|
@@ -4049,6 +4068,7 @@ function startSddRun(opts) {
|
|
|
4049
4068
|
});
|
|
4050
4069
|
const workflowId = kanbanWorkflowId("sdd", run.runId);
|
|
4051
4070
|
const legacyControl = opts.controlTransport === "legacy-file";
|
|
4071
|
+
const controlTransportName = legacyControl ? "legacy-file" : "kanban";
|
|
4052
4072
|
const legacyBoardState = opts.boardStateTransport === "legacy-file" || opts.boardStateTransport === void 0 && legacyControl;
|
|
4053
4073
|
const boardPersistence = legacyBoardState ? opts.boardStore : {
|
|
4054
4074
|
saveSnapshot: async (snapshot) => {
|
|
@@ -4110,7 +4130,7 @@ function startSddRun(opts) {
|
|
|
4110
4130
|
event: "sdd.control_drain_failed",
|
|
4111
4131
|
runId: run.runId,
|
|
4112
4132
|
workflowId,
|
|
4113
|
-
transport:
|
|
4133
|
+
transport: controlTransportName,
|
|
4114
4134
|
message,
|
|
4115
4135
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4116
4136
|
})
|
|
@@ -4121,12 +4141,12 @@ function startSddRun(opts) {
|
|
|
4121
4141
|
};
|
|
4122
4142
|
if (!legacyControl) {
|
|
4123
4143
|
void subscribeKanbanWorkflowCommands(opts.projectRoot, workflowId, () => {
|
|
4124
|
-
void drainControl()
|
|
4144
|
+
void drainControl();
|
|
4125
4145
|
}).then((unsubscribe) => {
|
|
4126
4146
|
if (controlDisposed) unsubscribe();
|
|
4127
4147
|
else {
|
|
4128
4148
|
unsubscribeControl = unsubscribe;
|
|
4129
|
-
void drainControl()
|
|
4149
|
+
void drainControl();
|
|
4130
4150
|
}
|
|
4131
4151
|
}).catch((error) => {
|
|
4132
4152
|
console.warn(
|
|
@@ -4159,7 +4179,7 @@ function startSddRun(opts) {
|
|
|
4159
4179
|
}
|
|
4160
4180
|
const drainMs = opts.controlDrainMs ?? 500;
|
|
4161
4181
|
const controlTimer = setInterval(() => {
|
|
4162
|
-
void drainControl()
|
|
4182
|
+
void drainControl();
|
|
4163
4183
|
}, drainMs);
|
|
4164
4184
|
controlTimer.unref?.();
|
|
4165
4185
|
const completion = (async () => {
|
|
@@ -4522,7 +4542,7 @@ async function gatherProjectContext(projectRoot) {
|
|
|
4522
4542
|
function deriveTitle(value) {
|
|
4523
4543
|
const firstLine = value.split("\n").map((line) => line.trim()).find(Boolean);
|
|
4524
4544
|
if (!firstLine) return "New SDD Project";
|
|
4525
|
-
const sentence = firstLine.split(/(?<=[.!?])\s/)[0]
|
|
4545
|
+
const sentence = firstLine.split(/(?<=[.!?])\s/)[0];
|
|
4526
4546
|
return sentence.length <= 64 ? sentence : `${sentence.slice(0, 63).trimEnd()}\u2026`;
|
|
4527
4547
|
}
|
|
4528
4548
|
function intakeToInterviewKickoff(record) {
|
|
@@ -5495,8 +5515,8 @@ function tokenizeCommand(command) {
|
|
|
5495
5515
|
hasToken = true;
|
|
5496
5516
|
}
|
|
5497
5517
|
if (inSingle || inDouble) return void 0;
|
|
5498
|
-
|
|
5499
|
-
return argv
|
|
5518
|
+
argv.push(current);
|
|
5519
|
+
return argv;
|
|
5500
5520
|
}
|
|
5501
5521
|
function makeCompositeVerifier(parts) {
|
|
5502
5522
|
return async function verifyTask(info) {
|
|
@@ -5569,7 +5589,7 @@ function makeCommandVerifier(options = {}) {
|
|
|
5569
5589
|
return { ok: false, reason: `verification command is malformed: ${rawCommand}` };
|
|
5570
5590
|
}
|
|
5571
5591
|
const [executable, ...args] = argv;
|
|
5572
|
-
return await new Promise((
|
|
5592
|
+
return await new Promise((resolve3) => {
|
|
5573
5593
|
const child = spawn(executable, args, {
|
|
5574
5594
|
cwd: info.cwd,
|
|
5575
5595
|
shell: false,
|
|
@@ -5580,18 +5600,18 @@ function makeCommandVerifier(options = {}) {
|
|
|
5580
5600
|
const timer = setTimeout(() => {
|
|
5581
5601
|
timedOut = true;
|
|
5582
5602
|
child.kill();
|
|
5583
|
-
|
|
5603
|
+
resolve3({ ok: false, reason: `verification timed out: ${rawCommand}` });
|
|
5584
5604
|
}, timeoutMs);
|
|
5585
5605
|
child.on("exit", (code) => {
|
|
5586
5606
|
clearTimeout(timer);
|
|
5587
5607
|
if (timedOut) return;
|
|
5588
|
-
|
|
5608
|
+
resolve3(
|
|
5589
5609
|
code === 0 ? { ok: true } : { ok: false, reason: `verification failed (exit ${code}): ${rawCommand}` }
|
|
5590
5610
|
);
|
|
5591
5611
|
});
|
|
5592
5612
|
child.on("error", (err) => {
|
|
5593
5613
|
clearTimeout(timer);
|
|
5594
|
-
|
|
5614
|
+
resolve3({ ok: false, reason: `verification spawn error: ${String(err)}` });
|
|
5595
5615
|
});
|
|
5596
5616
|
});
|
|
5597
5617
|
};
|
|
@@ -5750,7 +5770,7 @@ async function decomposeNonAtomicTasks(opts) {
|
|
|
5750
5770
|
|
|
5751
5771
|
// src/conflict-resolver.ts
|
|
5752
5772
|
import { readFile as readFile6, writeFile } from "node:fs/promises";
|
|
5753
|
-
import { isAbsolute as
|
|
5773
|
+
import { isAbsolute as isAbsolute3, join as join6 } from "node:path";
|
|
5754
5774
|
import { readBundledInstructionText as readBundledInstructionText2, renderInstructionTemplate as renderInstructionTemplate2 } from "@wrongstack/core/utils";
|
|
5755
5775
|
var defaultFileIO = {
|
|
5756
5776
|
read: (path6) => readFile6(path6, "utf8"),
|
|
@@ -5799,7 +5819,7 @@ function makePreferSideConflictResolver(side, io = defaultFileIO) {
|
|
|
5799
5819
|
return async function conflictResolver(info) {
|
|
5800
5820
|
if (info.conflictFiles.length === 0) return false;
|
|
5801
5821
|
for (const rel of info.conflictFiles) {
|
|
5802
|
-
const abs =
|
|
5822
|
+
const abs = isAbsolute3(rel) ? rel : join6(info.cwd, rel);
|
|
5803
5823
|
let content;
|
|
5804
5824
|
try {
|
|
5805
5825
|
content = await io.read(abs);
|
|
@@ -5833,7 +5853,7 @@ function makeLlmConflictResolver(opts) {
|
|
|
5833
5853
|
return async function conflictResolver(info) {
|
|
5834
5854
|
if (info.conflictFiles.length === 0) return false;
|
|
5835
5855
|
for (const rel of info.conflictFiles) {
|
|
5836
|
-
const abs =
|
|
5856
|
+
const abs = isAbsolute3(rel) ? rel : join6(info.cwd, rel);
|
|
5837
5857
|
let content;
|
|
5838
5858
|
try {
|
|
5839
5859
|
content = await io.read(abs);
|
package/dist/spec-store.d.ts
CHANGED
|
@@ -28,6 +28,14 @@ export declare class SpecStore {
|
|
|
28
28
|
createDraft(title: string, overview?: string): Promise<Specification>;
|
|
29
29
|
/** Update spec fields and persist. */
|
|
30
30
|
update(id: string, patch: Partial<Omit<Specification, 'id' | 'createdAt'>>): Promise<Specification | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a spec id to its file, refusing anything that escapes `baseDir`.
|
|
33
|
+
* `id` arrives from a WebSocket frame (`specs-ws-handler.ts:96-97,152`,
|
|
34
|
+
* raw `as string` casts) and from any persisted spec JSON. A bare
|
|
35
|
+
* `path.join` was the traversal primitive: `id = "../../../secret"`
|
|
36
|
+
* resolved outside the store and `load()` returned its contents. Mirrors
|
|
37
|
+
* the same containment `task-graph-store.ts:134-145` applies to its ids.
|
|
38
|
+
*/
|
|
31
39
|
private filePath;
|
|
32
40
|
private readIndex;
|
|
33
41
|
private updateIndex;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/sdd",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "WrongStack Spec-Driven Development engine — standalone package extracted from @wrongstack/core. Task graph generation, tracking, execution, lifecycle management, and AI-driven spec building for SDD workflows.",
|
|
6
6
|
"repository": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"!dist/**/*.map"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@wrongstack/core": "1.0.
|
|
31
|
-
"@wrongstack/kanban": "1.0.
|
|
32
|
-
"@wrongstack/primitives": "1.0.
|
|
33
|
-
"@wrongstack/requirement-intake": "1.0.
|
|
30
|
+
"@wrongstack/core": "1.0.3",
|
|
31
|
+
"@wrongstack/kanban": "1.0.3",
|
|
32
|
+
"@wrongstack/primitives": "1.0.3",
|
|
33
|
+
"@wrongstack/requirement-intake": "1.0.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^26.2.0",
|