scrumrun 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CORE.md +17 -3
  3. package/DECISIONS.md +56 -0
  4. package/MIGRATION-1-to-2.md +11 -0
  5. package/README.md +23 -5
  6. package/SPEC.md +30 -10
  7. package/bin/scrumrun.js +175 -11
  8. package/docs/COMMANDS.md +10 -4
  9. package/docs/ENTITY-MODEL.md +1 -1
  10. package/docs/RELEASE-SCORECARD.md +43 -0
  11. package/docs/RELEASE.md +19 -12
  12. package/docs/SCHEMA.md +11 -0
  13. package/docs/SEMANTIC-MEMORY.md +1 -1
  14. package/docs/TROUBLESHOOTING.md +13 -1
  15. package/lib/commands/manifest.js +15 -3
  16. package/lib/commands/render.js +4 -0
  17. package/lib/memory/index.js +201 -41
  18. package/lib/memory/service.js +3 -0
  19. package/lib/runtime/budgets.js +4 -0
  20. package/lib/runtime/canonical-snapshot.js +110 -0
  21. package/lib/runtime/context.js +5 -45
  22. package/lib/runtime/mutation-gateway.js +434 -0
  23. package/lib/runtime/orchestrator.js +130 -65
  24. package/lib/runtime/policy-engine.js +267 -0
  25. package/lib/runtime/request-engine.js +32 -24
  26. package/lib/runtime/review-service.js +92 -0
  27. package/lib/runtime/run-ledger.js +546 -0
  28. package/lib/runtime/workspace-state.js +146 -0
  29. package/lib/security/secrets.js +15 -1
  30. package/lib/v2/artifacts.js +24 -1
  31. package/lib/v2/conformance.js +78 -12
  32. package/lib/v2/migration.js +74 -10
  33. package/lib/v2/run-ledger-migration.js +268 -0
  34. package/lib/v2/schema.js +28 -1
  35. package/lib/v2/transaction.js +254 -0
  36. package/package.json +1 -1
  37. package/scripts/generate-contract-docs.js +11 -0
  38. package/templates/project/.scrumrun/guardrails.md +8 -0
  39. package/templates/project/.scrumrun/map.md +4 -3
  40. package/templates/project/.scrumrun/method.json +7 -1
  41. package/templates/project/.scrumrun/state.md +7 -14
  42. package/templates/project/AGENTS.md +2 -1
  43. package/templates/project-lean/AGENTS.md +3 -1
  44. package/templates/shared/skills/scrumrun/SKILL.md +19 -5
@@ -3,51 +3,56 @@
3
3
  const { sha256 } = require("../v2/artifacts");
4
4
  const { buildContextPackage } = require("./context");
5
5
  const { containsSecret } = require("../security/secrets");
6
+ const { evaluatePolicy, normalized } = require("./policy-engine");
7
+ const { workspaceState } = require("./workspace-state");
6
8
 
7
9
  function assessRisk(request) {
8
- const text = request.toLowerCase();
9
- const high = ["delete", "drop ", "migration", "migrate", "payment", "pricing", "auth", "permission", "security", "secret", "production", "publish", "release", "deploy"];
10
- const medium = ["database", "schema", "api", "cache", "performance", "refactor", "dependency", "upgrade", "rename", "move"];
11
- const reasons = [];
12
- for (const term of high) if (text.includes(term)) reasons.push(`high-impact term: ${term.trim()}`);
13
- if (reasons.length) return { level: "high", reasons: [...new Set(reasons)].slice(0, 6) };
14
- for (const term of medium) if (text.includes(term)) reasons.push(`cross-cutting term: ${term}`);
15
- if (reasons.length) return { level: "medium", reasons: [...new Set(reasons)].slice(0, 6) };
10
+ const text = normalized(request);
11
+ const high = [
12
+ ["destructive", /\b(delete|drop|truncate|erase|wipe|apagar|excluir|remover tudo)\b/],
13
+ ["migration", /\b(migrat\w*|migrac\w*)\b/],
14
+ ["external-release", /\b(publish|release|deploy|publicar|lancar|implant|producao)\b/],
15
+ ["security-boundary", /\b(auth|authorization|permission|security|secret|credential|autentic|autoriz|permiss|seguranca|segredo|credencial)\b/],
16
+ ["money", /\b(payment|pricing|billing|pagamento|preco|faturamento)\b/]
17
+ ];
18
+ const medium = [
19
+ ["data-or-contract", /\b(database|schema|api|contract|banco|contrato)\b/],
20
+ ["runtime-kernel", /\b(runtime|kernel|transaction|journal|recovery|multi-file|transacion|recuperac|multi-arquivo)\b/],
21
+ ["performance-or-cache", /\b(cache|performance|latency|index|desempenho|latencia|indice)\b/],
22
+ ["dependency-or-upgrade", /\b(dependency|upgrade|package|version|dependencia|atualiz|versao)\b/],
23
+ ["cross-cutting-change", /\b(refactor|rename|move|architecture|cli|refator|renome|mover|arquitetura)\b/]
24
+ ];
25
+ const highSignals = high.filter(([, matcher]) => matcher.test(text)).map(([signal]) => `high-impact signal: ${signal}`);
26
+ if (highSignals.length) return { level: "high", reasons: highSignals.slice(0, 6) };
27
+ const mediumSignals = medium.filter(([, matcher]) => matcher.test(text)).map(([signal]) => `cross-cutting signal: ${signal}`);
28
+ if (mediumSignals.length) return { level: "medium", reasons: mediumSignals.slice(0, 6) };
16
29
  return { level: "low", reasons: ["no high-impact signals detected; validate scope before execution"] };
17
30
  }
18
31
 
19
32
  function classifyRequest(request) {
20
- const text = request.toLowerCase();
33
+ const text = normalized(request);
21
34
  if (/\b(investigat|analy[sz]|study|discover|pesquis|analis|estud|descobr)/.test(text)) {
22
35
  return { type: "discovery", taskType: "discovery", reason: "request is primarily knowledge-seeking" };
23
36
  }
24
- if (/\b(bug|fix|broken|regression|erro|falha|corrigir|consert)/.test(text)) {
37
+ if (/\b(bug|fix|broken|regression|erro|falha|corrigir|consertar)\b/.test(text)) {
25
38
  return { type: "fix", taskType: "fix", reason: "request describes corrective work" };
26
39
  }
27
- if (/\b(feature|initiative|epic|iniciativa|funcionalidade)\b/.test(text)) {
40
+ if (/\b(initiative|epic|roadmap|program|iniciativa|programa)\b/.test(text)) {
28
41
  return { type: "feature", taskType: "feature", reason: "request describes a long-lived product initiative" };
29
42
  }
30
- if (/\b(sprint|timebox|batch|release train|lote)\b/.test(text)) {
43
+ if (/\b(sprint|timebox|batch|release train|lote)\b/.test(text) && /\b(tasks?|tarefas?|entregas?)\b/.test(text)) {
31
44
  return { type: "sprint", taskType: "task", reason: "request explicitly asks for a timebox or delivery batch" };
32
45
  }
33
- if (/\b(document|docs|readme|typo|texto|documenta)/.test(text)) {
46
+ const documentation = /\b(document|docs|readme|typo|texto|documenta)/.test(text);
47
+ const implementation = /\b(implement\w*|build\w*|creat\w*|add\w*|runtime|kernel|schema|migrat\w*|migrac\w*|transaction\w*|code|cli|criar|adicionar|construir|codigo|transacion\w*)\b/.test(text);
48
+ if (documentation && !implementation) {
34
49
  return { type: "task", taskType: "docs", reason: "request appears bounded to documentation" };
35
50
  }
36
51
  return { type: "task", taskType: "task", reason: "request is best represented as one atomic Task" };
37
52
  }
38
53
 
39
54
  function applyPolicy(context) {
40
- const violations = [];
41
- if (context.layout === "v1") violations.push("Project must be explicitly migrated to v2 before approved execution.");
42
- if (containsSecret(context.request)) violations.push("Request appears to contain a secret; remove/redact it before planning or persistence.");
43
- if (context.warnings.some((warning) => warning.includes("Secret-like content"))) violations.push("Canonical project context contains secret-like content; remove it before planning.");
44
- if (!context.guardrails.length) violations.push("No active canonical guardrail could be loaded.");
45
- if (context.warnings.some((warning) => warning.includes("guardrails.md"))) violations.push("Canonical guardrails are missing.");
46
- return {
47
- status: violations.length ? "blocked" : "passed",
48
- checked: context.guardrails.map((guardrail) => guardrail.id),
49
- violations
50
- };
55
+ return evaluatePolicy(context);
51
56
  }
52
57
 
53
58
  function stableJson(value) {
@@ -67,6 +72,7 @@ function encodeApproval(plan) {
67
72
  classification: plan.classification,
68
73
  risk: plan.risk,
69
74
  policy: plan.policy,
75
+ workspaceFingerprint: plan.workspaceFingerprint,
70
76
  issuedAt: plan.issuedAt
71
77
  };
72
78
  const encoded = Buffer.from(stableJson(payload)).toString("base64url");
@@ -94,6 +100,7 @@ function planRequest(projectRoot, request) {
94
100
  if (!normalized) throw new Error("A non-empty request is required for intake.");
95
101
  if (normalized.length > 10000) throw new Error("Request exceeds the 10,000 character intake limit.");
96
102
  const context = buildContextPackage(projectRoot, normalized);
103
+ const workspaceFingerprint = workspaceState(projectRoot).fingerprint;
97
104
  const policy = applyPolicy(context);
98
105
  const risk = assessRisk(normalized);
99
106
  const classification = classifyRequest(normalized);
@@ -102,6 +109,7 @@ function planRequest(projectRoot, request) {
102
109
  pipeline: ["received", "contextualizing", "policy", "risk", "classification", "planning", policy.status === "passed" ? "awaiting_approval" : "blocked"],
103
110
  request: normalized,
104
111
  context,
112
+ workspaceFingerprint,
105
113
  policy,
106
114
  risk,
107
115
  classification,
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ const path = require("node:path");
4
+ const { ArtifactRepository, METHOD_VERSION, withArtifactLock } = require("../v2/artifacts");
5
+ const { auditProject } = require("../v2/conformance");
6
+ const { assertCanonicalWrite } = require("./mutation-gateway");
7
+
8
+ function today() {
9
+ return new Date().toISOString().slice(0, 10);
10
+ }
11
+
12
+ function oneLine(value, label) {
13
+ const text = String(value || "").replace(/\s+/g, " ").trim();
14
+ if (!text) throw new Error(`${label} is required.`);
15
+ if (text.length > 500) throw new Error(`${label} exceeds 500 characters.`);
16
+ return text;
17
+ }
18
+
19
+ function nextReviewId(repository) {
20
+ const highest = repository.list("review").reduce((max, artifact) => {
21
+ const match = artifact.record.id.match(/-(\d+)$/);
22
+ return match ? Math.max(max, Number(match[1])) : max;
23
+ }, 0);
24
+ return `REV-${String(highest + 1).padStart(3, "0")}`;
25
+ }
26
+
27
+ function recordArtifactReviewUnlocked(projectRoot, options) {
28
+ const scrumDir = path.join(projectRoot, ".scrumrun");
29
+ const repository = new ArtifactRepository(scrumDir);
30
+ const taskId = oneLine(options.task, "task");
31
+ const task = repository.read("task", taskId);
32
+ if (!task || task.errors.length) throw new Error(`Review Task is missing or invalid: ${taskId}`);
33
+ const runId = options.run ? oneLine(options.run, "run") : null;
34
+ if (runId) {
35
+ const run = repository.read("run", runId);
36
+ if (!run || run.errors.length) throw new Error(`Review Run is missing or invalid: ${runId}`);
37
+ if (run.record.task !== taskId) throw new Error(`${runId} belongs to ${run.record.task}, not ${taskId}.`);
38
+ }
39
+ const title = oneLine(options.title || `Artifact conformance review for ${runId || taskId}`, "title");
40
+ const suppliedEvidence = (Array.isArray(options.evidence) ? options.evidence : []).map((item) => oneLine(item, "evidence"));
41
+ const audit = auditProject(projectRoot);
42
+ const created = today();
43
+ const record = {
44
+ id: nextReviewId(repository),
45
+ kind: "review",
46
+ status: audit.passed ? "passed" : "failed",
47
+ created,
48
+ updated: created,
49
+ method: METHOD_VERSION,
50
+ task: taskId
51
+ };
52
+ const findings = audit.findings.length
53
+ ? audit.findings.map((finding) => `- ${finding.severity}: ${finding.code} — ${finding.message}`)
54
+ : ["- No conformance findings."];
55
+ const evidence = [
56
+ `- Artifact audit: ${audit.invariants} invariants; ${audit.findings.length} finding(s); passed=${audit.passed}.`,
57
+ ...(runId ? [`- Run under review: ${runId}.`] : []),
58
+ ...suppliedEvidence.map((item) => `- ${item}`)
59
+ ];
60
+ const verdict = audit.passed
61
+ ? "Passed. The machine-readable artifact audit has no blocking finding."
62
+ : "Failed. Blocking conformance findings must be resolved in a separately authorized mutation.";
63
+ const body = [
64
+ `# ${title}`,
65
+ "",
66
+ "## Scope",
67
+ "",
68
+ `Review canonical project conformance for ${runId || taskId}.`,
69
+ "",
70
+ "## Evidence",
71
+ "",
72
+ ...evidence,
73
+ "",
74
+ "## Findings",
75
+ "",
76
+ ...findings,
77
+ "",
78
+ "## Verdict",
79
+ "",
80
+ verdict
81
+ ].join("\n");
82
+ assertCanonicalWrite(projectRoot, "record-artifact-review", [title, ...suppliedEvidence, body]);
83
+ repository.write(record, body);
84
+ return { review: repository.read("review", record.id), audit };
85
+ }
86
+
87
+ function recordArtifactReview(projectRoot, options = {}) {
88
+ const scrumDir = path.join(projectRoot, ".scrumrun");
89
+ return withArtifactLock(scrumDir, "review-artifact", () => recordArtifactReviewUnlocked(projectRoot, options));
90
+ }
91
+
92
+ module.exports = { recordArtifactReview };