@mstar-harness/engine 3.1.2 → 3.2.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/dist/audit.d.ts +46 -4
- package/dist/audit.js +755 -0
- package/dist/core.d.ts +2 -2
- package/dist/engine.js +176 -110
- package/dist/index.d.ts +2 -2
- package/dist/lint.d.ts +2 -2
- package/dist/status.d.ts +20 -0
- package/package.json +7 -3
package/dist/core.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Severity levels used across harness validation results.
|
|
3
3
|
*
|
|
4
|
-
* Machine SSOT — `mstar-
|
|
4
|
+
* Machine SSOT — `mstar-artifacts/references/status-and-residuals.md`
|
|
5
5
|
* § "Residual findings: `severity` (SSOT, machine field)" defines the same
|
|
6
6
|
* five lowercase-English values; `warning` / `Major` / any other value are
|
|
7
7
|
* forbidden in JSON severity fields.
|
|
@@ -83,7 +83,7 @@ export declare function readJson(filePath: string): Record<string, unknown>;
|
|
|
83
83
|
* stored state; revisit if the harness moves to a filesystem without
|
|
84
84
|
* rename-atomicity guarantees.
|
|
85
85
|
*/
|
|
86
|
-
export declare function writeJson(filePath: string, value:
|
|
86
|
+
export declare function writeJson<T>(filePath: string, value: T): void;
|
|
87
87
|
/**
|
|
88
88
|
* Resolve the project root by walking up from `startDir` (default: cwd) to
|
|
89
89
|
* the nearest ancestor containing `package.json` or `bun.lock`. Falls back
|
package/dist/engine.js
CHANGED
|
@@ -1350,41 +1350,42 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1350
1350
|
violations.push(violation4("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} — the root entry mirrors the snapshot; align them`));
|
|
1351
1351
|
}
|
|
1352
1352
|
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
1353
|
-
violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — the root entry mirrors the snapshot
|
|
1353
|
+
violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot — align them or remove the colliding workflow`));
|
|
1354
1354
|
}
|
|
1355
1355
|
}
|
|
1356
1356
|
}
|
|
1357
1357
|
return { ok: violations.length === 0, violations };
|
|
1358
1358
|
}
|
|
1359
1359
|
var validateStatus = validateStatusV2;
|
|
1360
|
+
function registerWorkflowEntryLocked(statusPath, entry) {
|
|
1361
|
+
const harnessDir = dirname5(statusPath);
|
|
1362
|
+
const current = readJson(statusPath);
|
|
1363
|
+
const fresh = Object.keys(current).length === 0;
|
|
1364
|
+
const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
|
|
1365
|
+
if (!fresh && !Array.isArray(doc.workflows)) {
|
|
1366
|
+
throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
|
|
1367
|
+
}
|
|
1368
|
+
const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
|
|
1369
|
+
if (existing >= 0) {
|
|
1370
|
+
doc.workflows[existing] = entry;
|
|
1371
|
+
} else {
|
|
1372
|
+
doc.workflows.push(entry);
|
|
1373
|
+
}
|
|
1374
|
+
doc.updated_at = todayString();
|
|
1375
|
+
const gate = validateStatusV2(doc, { harnessDir });
|
|
1376
|
+
if (!gate.ok) {
|
|
1377
|
+
throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1378
|
+
}
|
|
1379
|
+
writeJson(statusPath, doc);
|
|
1380
|
+
return doc;
|
|
1381
|
+
}
|
|
1360
1382
|
async function registerWorkflow(root, entry) {
|
|
1361
1383
|
const entryGate = validateWorkflowEntry(entry);
|
|
1362
1384
|
if (!entryGate.ok) {
|
|
1363
1385
|
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
1364
1386
|
}
|
|
1365
1387
|
const statusPath = resolve5(root);
|
|
1366
|
-
|
|
1367
|
-
return withStatusWriteLock(statusPath, () => {
|
|
1368
|
-
const current = readJson(statusPath);
|
|
1369
|
-
const fresh = Object.keys(current).length === 0;
|
|
1370
|
-
const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
|
|
1371
|
-
if (!fresh && !Array.isArray(doc.workflows)) {
|
|
1372
|
-
throw new Error("refusing to modify status.json: workflows must be an array — a v1 root must be migrated first (run `mstar migrate`)");
|
|
1373
|
-
}
|
|
1374
|
-
const existing = doc.workflows.findIndex((wf) => wf.id === entry.id);
|
|
1375
|
-
if (existing >= 0) {
|
|
1376
|
-
doc.workflows[existing] = entry;
|
|
1377
|
-
} else {
|
|
1378
|
-
doc.workflows.push(entry);
|
|
1379
|
-
}
|
|
1380
|
-
doc.updated_at = todayString();
|
|
1381
|
-
const gate = validateStatusV2(doc, { harnessDir });
|
|
1382
|
-
if (!gate.ok) {
|
|
1383
|
-
throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1384
|
-
}
|
|
1385
|
-
writeJson(statusPath, doc);
|
|
1386
|
-
return doc;
|
|
1387
|
-
});
|
|
1388
|
+
return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
|
|
1388
1389
|
}
|
|
1389
1390
|
async function unregisterWorkflow(root, id) {
|
|
1390
1391
|
if (typeof id !== "string" || id.trim() === "") {
|
|
@@ -2990,9 +2991,11 @@ async function applyMigratePlan(plan) {
|
|
|
2990
2991
|
if (!gate2.ok) {
|
|
2991
2992
|
throw new Error(`refusing to apply migration: invalid project register: ${gate2.violations.map((v) => v.message).join("; ")}`);
|
|
2992
2993
|
}
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2994
|
+
if (Object.keys(plan.register.data.entries ?? {}).length > 0) {
|
|
2995
|
+
const filePath = projectTargetOf(plan.register.file);
|
|
2996
|
+
mkdirSync6(dirname7(filePath), { recursive: true });
|
|
2997
|
+
writeJson(filePath, plan.register.data);
|
|
2998
|
+
}
|
|
2996
2999
|
}
|
|
2997
3000
|
if (plan.roadmap !== null) {
|
|
2998
3001
|
const filePath = projectTargetOf(plan.roadmap.file);
|
|
@@ -3419,8 +3422,8 @@ function completenessLevel(frontmatterText, checklist) {
|
|
|
3419
3422
|
return { level, items, missing, placeholders, upgradeTo, bodyUnverified };
|
|
3420
3423
|
}
|
|
3421
3424
|
// src/audit.ts
|
|
3422
|
-
import { mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3423
|
-
import { join as join11, resolve as resolve9 } from "node:path";
|
|
3425
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3426
|
+
import { basename as basename4, join as join11, resolve as resolve9, sep as sep3 } from "node:path";
|
|
3424
3427
|
function violation9(severity, code, message, fix) {
|
|
3425
3428
|
return { ok: false, severity, code, message, fix };
|
|
3426
3429
|
}
|
|
@@ -3465,14 +3468,14 @@ function validateAuditStatusBlocks(planText) {
|
|
|
3465
3468
|
const violations = [];
|
|
3466
3469
|
const blocks = parseStatusBlocks(planText);
|
|
3467
3470
|
if (blocks.length === 0) {
|
|
3468
|
-
violations.push(violation9("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL § Plan
|
|
3471
|
+
violations.push(violation9("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL.md § Plan output)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
|
|
3469
3472
|
return { ok: false, violations };
|
|
3470
3473
|
}
|
|
3471
3474
|
blocks.forEach((block, index) => {
|
|
3472
3475
|
const label = blocks.length > 1 ? ` #${index + 1}` : "";
|
|
3473
3476
|
for (const field of AUDIT_STATUS_FIELDS) {
|
|
3474
3477
|
if (!block.fields.has(field)) {
|
|
3475
|
-
violations.push(violation9("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL § Plan
|
|
3478
|
+
violations.push(violation9("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL.md § Plan output)`, `add \`- **${field}**: <value>\` to the Status block`));
|
|
3476
3479
|
}
|
|
3477
3480
|
}
|
|
3478
3481
|
const check = (field, pattern, code, expected) => {
|
|
@@ -3480,7 +3483,7 @@ function validateAuditStatusBlocks(planText) {
|
|
|
3480
3483
|
if (value === undefined)
|
|
3481
3484
|
return;
|
|
3482
3485
|
if (!pattern.test(value)) {
|
|
3483
|
-
violations.push(violation9("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL § Plan
|
|
3486
|
+
violations.push(violation9("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL.md § Plan output)`, `fix \`- **${field}**:\` to one of: ${expected}`));
|
|
3484
3487
|
}
|
|
3485
3488
|
};
|
|
3486
3489
|
check("Priority", /^P[123]$/, "audit.status.invalid-priority", "P1 | P2 | P3");
|
|
@@ -3492,74 +3495,6 @@ function validateAuditStatusBlocks(planText) {
|
|
|
3492
3495
|
});
|
|
3493
3496
|
return { ok: violations.length === 0, violations };
|
|
3494
3497
|
}
|
|
3495
|
-
var WHOLE_MATCH_PATTERNS = [
|
|
3496
|
-
{ type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
|
|
3497
|
-
{ type: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
3498
|
-
{ type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
|
|
3499
|
-
{ type: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
3500
|
-
{ type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\b/g },
|
|
3501
|
-
{ type: "api-secret-key", re: /\bsk-[A-Za-z0-9-]{20,}\b/g }
|
|
3502
|
-
];
|
|
3503
|
-
var VALUE_PATTERNS = [
|
|
3504
|
-
{
|
|
3505
|
-
typeOf: (key) => key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[_-]+/g, "-"),
|
|
3506
|
-
re: /(["']?)\b(password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|secret|token)\b(["']?)(\s*[:=]\s*)("[^"\n]{8,}"|'[^'\n]{8,}'|[A-Za-z0-9_./+\-=]{16,})/gi
|
|
3507
|
-
}
|
|
3508
|
-
];
|
|
3509
|
-
function buildLineStarts(text) {
|
|
3510
|
-
const starts = [0];
|
|
3511
|
-
for (let i = 0;i < text.length; i++) {
|
|
3512
|
-
if (text[i] === `
|
|
3513
|
-
`)
|
|
3514
|
-
starts.push(i + 1);
|
|
3515
|
-
}
|
|
3516
|
-
return starts;
|
|
3517
|
-
}
|
|
3518
|
-
function lineAt(starts, index) {
|
|
3519
|
-
let lo = 0;
|
|
3520
|
-
let hi = starts.length - 1;
|
|
3521
|
-
while (lo < hi) {
|
|
3522
|
-
const mid = lo + hi + 1 >> 1;
|
|
3523
|
-
if (starts[mid] <= index)
|
|
3524
|
-
lo = mid;
|
|
3525
|
-
else
|
|
3526
|
-
hi = mid - 1;
|
|
3527
|
-
}
|
|
3528
|
-
return lo + 1;
|
|
3529
|
-
}
|
|
3530
|
-
function redactSecrets(text, filePath) {
|
|
3531
|
-
const starts = buildLineStarts(text);
|
|
3532
|
-
const marker = (type, index) => `[REDACTED ${type}@${lineAt(starts, index)}${filePath === undefined ? "" : ` in ${filePath}`}]`;
|
|
3533
|
-
const replacements = [];
|
|
3534
|
-
const findings = [];
|
|
3535
|
-
for (const pattern of WHOLE_MATCH_PATTERNS) {
|
|
3536
|
-
for (const match of text.matchAll(pattern.re)) {
|
|
3537
|
-
if (match.index === undefined)
|
|
3538
|
-
continue;
|
|
3539
|
-
replacements.push({ index: match.index, length: match[0].length, text: marker(pattern.type, match.index) });
|
|
3540
|
-
findings.push({ line: lineAt(starts, match.index), type: pattern.type });
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
for (const pattern of VALUE_PATTERNS) {
|
|
3544
|
-
for (const match of text.matchAll(pattern.re)) {
|
|
3545
|
-
if (match.index === undefined)
|
|
3546
|
-
continue;
|
|
3547
|
-
const type = pattern.typeOf(match[2]);
|
|
3548
|
-
const replacement = `${match[1]}${match[2]}${match[3]}${match[4]}${marker(type, match.index)}`;
|
|
3549
|
-
replacements.push({ index: match.index, length: match[0].length, text: replacement });
|
|
3550
|
-
findings.push({ line: lineAt(starts, match.index), type });
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
replacements.sort((a, b) => b.index - a.index);
|
|
3554
|
-
let out = text;
|
|
3555
|
-
for (const r of replacements)
|
|
3556
|
-
out = out.slice(0, r.index) + r.text + out.slice(r.index + r.length);
|
|
3557
|
-
const deduped = new Map;
|
|
3558
|
-
for (const f of findings)
|
|
3559
|
-
deduped.set(`${f.line}:${f.type}`, f);
|
|
3560
|
-
const sorted = [...deduped.values()].sort((a, b) => a.line - b.line || a.type.localeCompare(b.type));
|
|
3561
|
-
return { text: out, findings: sorted };
|
|
3562
|
-
}
|
|
3563
3498
|
function slugify(title) {
|
|
3564
3499
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3565
3500
|
}
|
|
@@ -3696,9 +3631,140 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
3696
3631
|
}));
|
|
3697
3632
|
return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
|
|
3698
3633
|
}
|
|
3634
|
+
async function promoteAuditPlans(outDir, selected, options) {
|
|
3635
|
+
if (selected.length === 0) {
|
|
3636
|
+
throw new Error("promoteAuditPlans: at least one plan id must be selected (--plans 001,002,…)");
|
|
3637
|
+
}
|
|
3638
|
+
if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
|
|
3639
|
+
throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
|
|
3640
|
+
}
|
|
3641
|
+
const workflowId = options.workflowId ?? basename4(resolve9(outDir));
|
|
3642
|
+
assertSafePathComponent(workflowId, "workflow id");
|
|
3643
|
+
const harnessDir = resolve9(options.harnessDir);
|
|
3644
|
+
const statusPath = join11(harnessDir, "status.json");
|
|
3645
|
+
const workflowDir = join11(harnessDir, "workflows", workflowId);
|
|
3646
|
+
const snapshotPath = join11(workflowDir, WORKFLOW_SNAPSHOT_FILE);
|
|
3647
|
+
const planFiles = resolveSelectedPlanFiles(outDir, selected);
|
|
3648
|
+
const indexRows = readExecutionOrderIndex(outDir);
|
|
3649
|
+
const plans = planFiles.map((planFile) => {
|
|
3650
|
+
const stem = planFile.replace(/\.md$/, "");
|
|
3651
|
+
const num = stem.slice(0, 3);
|
|
3652
|
+
const indexRow = indexRows.get(num);
|
|
3653
|
+
const title = indexRow?.title ?? readPlanFileSummary(join11(outDir, planFile)).title;
|
|
3654
|
+
return {
|
|
3655
|
+
id: stem,
|
|
3656
|
+
title,
|
|
3657
|
+
file: planFileRel(outDir, planFile),
|
|
3658
|
+
status: "Todo"
|
|
3659
|
+
};
|
|
3660
|
+
});
|
|
3661
|
+
const now = new Date;
|
|
3662
|
+
const snapshot = {
|
|
3663
|
+
schema_version: 1,
|
|
3664
|
+
id: workflowId,
|
|
3665
|
+
type: "plan",
|
|
3666
|
+
status: "running",
|
|
3667
|
+
started_at: now.toISOString(),
|
|
3668
|
+
updated_at: now.toISOString().slice(0, 10),
|
|
3669
|
+
plans
|
|
3670
|
+
};
|
|
3671
|
+
const entry = {
|
|
3672
|
+
id: workflowId,
|
|
3673
|
+
type: "plan",
|
|
3674
|
+
started_at: snapshot.started_at,
|
|
3675
|
+
dir: `workflows/${workflowId}`
|
|
3676
|
+
};
|
|
3677
|
+
const entryGate = validateWorkflowEntry(entry);
|
|
3678
|
+
if (!entryGate.ok) {
|
|
3679
|
+
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
3680
|
+
}
|
|
3681
|
+
await withStatusWriteLock(statusPath, () => {
|
|
3682
|
+
if (existsSync7(snapshotPath)) {
|
|
3683
|
+
throw new Error(`refusing to promote audit plans: workflow ${JSON.stringify(workflowId)} already exists ` + `(snapshot at ${snapshotPath}) — re-promote would drop its registered plan rows; ` + `remove that workflow before promoting again`);
|
|
3684
|
+
}
|
|
3685
|
+
mkdirSync7(workflowDir, { recursive: true });
|
|
3686
|
+
try {
|
|
3687
|
+
writeJson(snapshotPath, snapshot);
|
|
3688
|
+
registerWorkflowEntryLocked(statusPath, entry);
|
|
3689
|
+
} catch (error) {
|
|
3690
|
+
rmSync(snapshotPath, { force: true });
|
|
3691
|
+
try {
|
|
3692
|
+
if (readdirSync7(workflowDir).length === 0) {
|
|
3693
|
+
rmdirSync2(workflowDir);
|
|
3694
|
+
}
|
|
3695
|
+
} catch {}
|
|
3696
|
+
throw error;
|
|
3697
|
+
}
|
|
3698
|
+
return { workflowId, snapshotPath };
|
|
3699
|
+
});
|
|
3700
|
+
return { workflowId, snapshotPath };
|
|
3701
|
+
}
|
|
3702
|
+
function resolveSelectedPlanFiles(outDir, selected) {
|
|
3703
|
+
const files = readdirSync7(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
|
|
3704
|
+
const byNum = new Map;
|
|
3705
|
+
const byStem = new Map;
|
|
3706
|
+
for (const file of files) {
|
|
3707
|
+
const stem = file.replace(/\.md$/, "");
|
|
3708
|
+
if (!byNum.has(stem.slice(0, 3))) {
|
|
3709
|
+
byNum.set(stem.slice(0, 3), file);
|
|
3710
|
+
}
|
|
3711
|
+
byStem.set(stem, file);
|
|
3712
|
+
}
|
|
3713
|
+
const resolved = [];
|
|
3714
|
+
const seen = new Set;
|
|
3715
|
+
for (const id of selected) {
|
|
3716
|
+
const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
|
|
3717
|
+
if (file === undefined) {
|
|
3718
|
+
throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve9(outDir)}`);
|
|
3719
|
+
}
|
|
3720
|
+
if (!seen.has(file)) {
|
|
3721
|
+
seen.add(file);
|
|
3722
|
+
resolved.push(file);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
return resolved;
|
|
3726
|
+
}
|
|
3727
|
+
function readExecutionOrderIndex(outDir) {
|
|
3728
|
+
const readmePath = join11(outDir, "README.md");
|
|
3729
|
+
let text;
|
|
3730
|
+
try {
|
|
3731
|
+
text = readFileSync9(readmePath, "utf8");
|
|
3732
|
+
} catch {
|
|
3733
|
+
return new Map;
|
|
3734
|
+
}
|
|
3735
|
+
const rows = new Map;
|
|
3736
|
+
const lines = text.split(`
|
|
3737
|
+
`);
|
|
3738
|
+
let inSection = false;
|
|
3739
|
+
for (const line of lines) {
|
|
3740
|
+
if (/^##\s+Execution order & status/.test(line)) {
|
|
3741
|
+
inSection = true;
|
|
3742
|
+
continue;
|
|
3743
|
+
}
|
|
3744
|
+
if (inSection && /^#/.test(line)) {
|
|
3745
|
+
break;
|
|
3746
|
+
}
|
|
3747
|
+
if (!inSection)
|
|
3748
|
+
continue;
|
|
3749
|
+
const cells = line.split(/(?<!\\)\|/).map((c) => c.trim());
|
|
3750
|
+
if (cells.length >= 3 && /^\d{3}$/.test(cells[1])) {
|
|
3751
|
+
rows.set(cells[1], { title: cells[2].replace(/\\\|/g, "|") });
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
return rows;
|
|
3755
|
+
}
|
|
3756
|
+
function planFileRel(outDir, planFile) {
|
|
3757
|
+
const resolved = resolve9(outDir);
|
|
3758
|
+
const parts = resolved.split(sep3);
|
|
3759
|
+
const plansIdx = parts.lastIndexOf("plans");
|
|
3760
|
+
if (plansIdx >= 0) {
|
|
3761
|
+
return `${parts.slice(plansIdx + 1).join(sep3)}${sep3}${planFile}`;
|
|
3762
|
+
}
|
|
3763
|
+
return planFile;
|
|
3764
|
+
}
|
|
3699
3765
|
// src/compound.ts
|
|
3700
|
-
import { existsSync as
|
|
3701
|
-
import { basename as
|
|
3766
|
+
import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
3767
|
+
import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
|
|
3702
3768
|
function violation10(severity, code, message, fix) {
|
|
3703
3769
|
return { ok: false, severity, code, message, fix };
|
|
3704
3770
|
}
|
|
@@ -4005,7 +4071,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4005
4071
|
for (const { ref, isSymbol, module } of refs) {
|
|
4006
4072
|
if (!isSymbol || module === undefined) {
|
|
4007
4073
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
4008
|
-
if (
|
|
4074
|
+
if (existsSync8(resolve10(repoRoot, candidate))) {
|
|
4009
4075
|
checked++;
|
|
4010
4076
|
} else {
|
|
4011
4077
|
violations.push(violation10("medium", "compound.reference.missing-file", `referenced path \`${ref}\` does not exist under ${repoRoot} (compound-refresh Phase 2: referenced code still exists?)`, "update the doc to reference an existing path, or delete the stale reference"));
|
|
@@ -4036,7 +4102,7 @@ function collectKnowledgeDocs(dir) {
|
|
|
4036
4102
|
if (entry.isDirectory()) {
|
|
4037
4103
|
stack.push(full);
|
|
4038
4104
|
} else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
|
|
4039
|
-
docs.push(relative4(dir, full).split(
|
|
4105
|
+
docs.push(relative4(dir, full).split(sep4).join("/"));
|
|
4040
4106
|
}
|
|
4041
4107
|
}
|
|
4042
4108
|
}
|
|
@@ -4053,7 +4119,7 @@ function normalizeIndexRef(cell) {
|
|
|
4053
4119
|
function assertIndexRows(knowledgeDir) {
|
|
4054
4120
|
const violations = [];
|
|
4055
4121
|
const readmePath = join12(knowledgeDir, "README.md");
|
|
4056
|
-
if (!
|
|
4122
|
+
if (!existsSync8(readmePath)) {
|
|
4057
4123
|
violations.push(violation10("medium", "compound.index.missing-readme", `missing ${readmePath} — the knowledge index is required (mstar-compound Phase 6: every doc gets a README.md row)`, "create knowledge/README.md with a Document / Source Plan / Description / Status table"));
|
|
4058
4124
|
return { ok: false, violations };
|
|
4059
4125
|
}
|
|
@@ -4085,7 +4151,7 @@ function compoundRefreshScope(harnessDir, projectRoot) {
|
|
|
4085
4151
|
];
|
|
4086
4152
|
}
|
|
4087
4153
|
function isFileLikeRoot(root) {
|
|
4088
|
-
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(
|
|
4154
|
+
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename5(root));
|
|
4089
4155
|
}
|
|
4090
4156
|
function scopeGuard(path, allowedRoots) {
|
|
4091
4157
|
const resolved = resolve10(path);
|
|
@@ -4094,7 +4160,7 @@ function scopeGuard(path, allowedRoots) {
|
|
|
4094
4160
|
if (isFileLikeRoot(r)) {
|
|
4095
4161
|
if (resolved === r)
|
|
4096
4162
|
return { ok: true, violations: [] };
|
|
4097
|
-
} else if (resolved === r || resolved.startsWith(r +
|
|
4163
|
+
} else if (resolved === r || resolved.startsWith(r + sep4)) {
|
|
4098
4164
|
return { ok: true, violations: [] };
|
|
4099
4165
|
}
|
|
4100
4166
|
}
|
|
@@ -4243,7 +4309,7 @@ function planQualityBar(planText) {
|
|
|
4243
4309
|
if (token !== null) {
|
|
4244
4310
|
const text = trimmed.length > 80 ? `${trimmed.slice(0, 77)}...` : trimmed;
|
|
4245
4311
|
findings.push({ token, line: i + 1, text });
|
|
4246
|
-
violations.push(violation11("medium", "lint.plan-quality.placeholder", `placeholder token "${token}" at line ${i + 1}: "${text}"`, "replace the placeholder with concrete content before locking the plan (mstar-
|
|
4312
|
+
violations.push(violation11("medium", "lint.plan-quality.placeholder", `placeholder token "${token}" at line ${i + 1}: "${text}"`, "replace the placeholder with concrete content before locking the plan (mstar-artifacts/references/plan-quality-bar.md; templates/plan.main.md placeholder scan)"));
|
|
4247
4313
|
}
|
|
4248
4314
|
}
|
|
4249
4315
|
return { ok: violations.length === 0, violations, findings };
|
|
@@ -4338,7 +4404,7 @@ function lintStrategySections(docText) {
|
|
|
4338
4404
|
return { ok: violations.length === 0, violations };
|
|
4339
4405
|
}
|
|
4340
4406
|
// src/roles.ts
|
|
4341
|
-
import { existsSync as
|
|
4407
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
4342
4408
|
import { join as join13 } from "node:path";
|
|
4343
4409
|
function violation12(severity, code, message, fix) {
|
|
4344
4410
|
return { ok: false, severity, code, message, fix };
|
|
@@ -4395,7 +4461,7 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
4395
4461
|
const violations = [];
|
|
4396
4462
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
4397
4463
|
for (const { agentId, reference } of mapping) {
|
|
4398
|
-
if (!
|
|
4464
|
+
if (!existsSync9(join13(rolesDir, reference))) {
|
|
4399
4465
|
violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join13(rolesDir, reference)} or fix the mapping row`));
|
|
4400
4466
|
}
|
|
4401
4467
|
}
|
|
@@ -4651,11 +4717,11 @@ export {
|
|
|
4651
4717
|
releaseLease,
|
|
4652
4718
|
registerWorkflow,
|
|
4653
4719
|
referenceExists,
|
|
4654
|
-
redactSecrets,
|
|
4655
4720
|
readProgressLedger,
|
|
4656
4721
|
readJson,
|
|
4657
4722
|
readHarnessVersion,
|
|
4658
4723
|
pushCadenceProbe,
|
|
4724
|
+
promoteAuditPlans,
|
|
4659
4725
|
planQualityBar,
|
|
4660
4726
|
planExecutionLeaseLocations,
|
|
4661
4727
|
parseMstarc,
|
package/dist/index.d.ts
CHANGED
|
@@ -46,8 +46,8 @@ export type { MigrateNotesFile, MigrateOptions, MigratePlan, MigrateRegister, Mi
|
|
|
46
46
|
export { ARCHIVED_STATUS_V1_FILE, MIGRATE_STATUS_FILE, NOTES_LEDGER_FILE, applyMigratePlan, migrateHarnessTree, } from "./migrate.js";
|
|
47
47
|
export type { CompletenessItem, CompletenessLevel, CompletenessPlaceholder, CompletenessResult, DesignFrontmatter, } from "./design-md.js";
|
|
48
48
|
export { assertLightDarkParity, completenessLevel, parseDesignFrontmatter, validateDesignTokenFrontmatter, } from "./design-md.js";
|
|
49
|
-
export type { AuditCategory, AuditEffort, AuditFinding, AuditPriority, AuditRisk, RedactResult, ScaffoldAuditPlanOptions, ScaffoldAuditPlanResult, SecretFinding, } from "./audit.js";
|
|
50
|
-
export { AUDIT_CATEGORIES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS,
|
|
49
|
+
export type { AuditCategory, AuditEffort, AuditFinding, AuditPriority, AuditRisk, PromoteAuditPlansOptions, RedactResult, ScaffoldAuditPlanOptions, ScaffoldAuditPlanResult, SecretFinding, } from "./audit.js";
|
|
50
|
+
export { AUDIT_CATEGORIES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS, promoteAuditPlans, scaffoldAuditPlan, validateAuditStatusBlocks, } from "./audit.js";
|
|
51
51
|
export type { ReferenceCheckResult } from "./compound.js";
|
|
52
52
|
export { KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_CATEGORY_MAP, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_SEVERITIES, assertIndexRows, compoundRefreshScope, referenceExists, scopeGuard, validateSchemaYaml, } from "./compound.js";
|
|
53
53
|
export type { EphemeralCitation, PlanQualityFinding, PlanQualityResult, SimplifyMarker, TemporaryMarker, TemporaryMarkerResult, } from "./lint.js";
|
package/dist/lint.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* completion evidence must include the TDD triple (test file(s), command,
|
|
19
19
|
* output) in `task-N-report.md`; `mstar-sdd/references/file-handoffs.md` —
|
|
20
20
|
* fix subagents append covering test file(s), command run, output.
|
|
21
|
-
* - Plan quality bar: `mstar-
|
|
21
|
+
* - Plan quality bar: `mstar-artifacts/references/plan-quality-bar.md`
|
|
22
22
|
* § Quality checklist + `templates/plan.main.md` self-review
|
|
23
23
|
* ("Placeholder scan: no TBD").
|
|
24
24
|
* - Skill frontmatter contract: `mstar-skill-authoring` SKILL.md § Frontmatter
|
|
@@ -180,7 +180,7 @@ export type PlanQualityResult = GateResult & {
|
|
|
180
180
|
findings: PlanQualityFinding[];
|
|
181
181
|
};
|
|
182
182
|
/**
|
|
183
|
-
* Plan quality bar — placeholder scan (mstar-
|
|
183
|
+
* Plan quality bar — placeholder scan (mstar-artifacts
|
|
184
184
|
* `references/plan-quality-bar.md` § Quality checklist + `templates/
|
|
185
185
|
* plan.main.md` self-review "Placeholder scan: no TBD"). Every placeholder
|
|
186
186
|
* token found becomes one `lint.plan-quality.placeholder` violation whose
|
package/dist/status.d.ts
CHANGED
|
@@ -136,6 +136,26 @@ export declare function validateStatusV2(docOrPath: StatusV2Doc | string, opts?:
|
|
|
136
136
|
* closed on v1 input with the `mstar migrate` hint.
|
|
137
137
|
*/
|
|
138
138
|
export declare const validateStatus: typeof validateStatusV2;
|
|
139
|
+
/**
|
|
140
|
+
* Root-file workflow upsert, to be called ONLY while the caller holds the
|
|
141
|
+
* root `withStatusWriteLock(statusPath)` (see `registerWorkflow` and the
|
|
142
|
+
* audit promote path, which call this from inside their lock — the root
|
|
143
|
+
* lock is the serialization point for read-check-replace-verify).
|
|
144
|
+
*
|
|
145
|
+
* Idempotent upsert by entry `id`, bumping root `updated_at`. A
|
|
146
|
+
* missing/empty root file is initialized from the v2 template (never a v1
|
|
147
|
+
* tree); a v1 root is refused with the `mstar migrate` hint (no silent
|
|
148
|
+
* mutation of an un-migrated tree). The final document is validated with
|
|
149
|
+
* `validateStatusV2` (including the removal-at-terminal snapshot invariant
|
|
150
|
+
* against `dirname(statusPath)`) before the write — an entry whose
|
|
151
|
+
* snapshot is missing or terminal is refused and nothing is written.
|
|
152
|
+
*
|
|
153
|
+
* The caller must validate the entry (`validateWorkflowEntry`) before
|
|
154
|
+
* acquiring the lock; this helper asserts it as a safety net (cheap —
|
|
155
|
+
* an invalid entry would fail `validateStatusV2` anyway, but the explicit
|
|
156
|
+
* gate keeps the pre-lock fail-fast contract of `registerWorkflow`).
|
|
157
|
+
*/
|
|
158
|
+
export declare function registerWorkflowEntryLocked(statusPath: string, entry: WorkflowEntry): StatusV2Doc;
|
|
139
159
|
/**
|
|
140
160
|
* Register one active workflow entry in the v2 root file (plan Task 3).
|
|
141
161
|
* Idempotent upsert by entry `id` under the root-file `withStatusWriteLock`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mstar-harness/engine",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Morning Star Harness Workflow Engine — deterministic workflow enforcement library (path, status, lease, dispatch, sdd, iteration, lint gates).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"default": "./dist/engine.js"
|
|
18
18
|
},
|
|
19
|
-
"./package.json": "./package.json"
|
|
19
|
+
"./package.json": "./package.json",
|
|
20
|
+
"./src/audit": {
|
|
21
|
+
"types": "./dist/audit.d.ts",
|
|
22
|
+
"default": "./dist/audit.js"
|
|
23
|
+
}
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
22
26
|
"dist"
|
|
23
27
|
],
|
|
24
28
|
"scripts": {
|
|
25
|
-
"build": "rm -rf dist && bun build src/index.ts --target node --outfile dist/engine.js && bunx tsc",
|
|
29
|
+
"build": "rm -rf dist && bun build src/index.ts --target node --outfile dist/engine.js && bun build src/audit.ts --target node --outfile dist/audit.js && bunx tsc",
|
|
26
30
|
"test": "bun test",
|
|
27
31
|
"prepublishOnly": "bun run build"
|
|
28
32
|
},
|