@luizsantiago/spec-guardrails 3.4.0 → 3.6.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/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  | **Solution** | One kit, two deliberate modes: **Process** (Node only) for a flexible spec-driven workflow; **Brakes** (Node + Python) for the **full product** — structural gates that exit non-zero when paperwork or evidence is missing. You approve specs/tasks in both. |
12
12
  | **Result** | Traceable `.specs/` memory, fewer fake finishes, cheaper turns (~70% less skill text on planning). Choose Process for light ceremony; add Python when you want the [Guarantees matrix](#guarantees-matrix) enforced automatically. |
13
13
 
14
- npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **3.4.x**
14
+ npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **3.6.x**
15
15
 
16
16
  ---
17
17
 
@@ -217,6 +217,8 @@ npx @luizsantiago/spec-guardrails install
217
217
 
218
218
  | Version | What you gain |
219
219
  | --- | --- |
220
+ | **3.6.x** | Hybrid retrieval (`memory-retrieve`); chunk FTS; optional semantic embed |
221
+ | **3.5.x** | Solution exploration (`solution-explore`) — compare candidates from approved spec |
220
222
  | **3.4.x** | Contextual guards (`context-guard`); FTS memory search (`memory-search`) |
221
223
  | **3.3.x** | Intent/effect policy — `check-path --op read|write|delete` and `effects` config block |
222
224
  | **3.2.x** | Single-package focus; artifact gate severity labels; git worktree isolation CLI; execution policy (budget/scope/escalation) |
package/index.js CHANGED
@@ -35,6 +35,16 @@ import {
35
35
  listWorkspaces,
36
36
  prepareWorkspaces,
37
37
  } from "./lib/workspace-isolation.js";
38
+ import {
39
+ formatExplorationInit,
40
+ formatExplorationStatus,
41
+ formatExplorationValidation,
42
+ getExplorationStatus,
43
+ initExploration,
44
+ normalizeCandidateId,
45
+ recordExplorationDecision,
46
+ validateExplorationArtifact,
47
+ } from "./lib/solution-exploration.js";
38
48
  import {
39
49
  initProjectConfig,
40
50
  listPresets,
@@ -93,12 +103,16 @@ Commands:
93
103
  execution-policy record-retry <task> Increment retry counter for a task id (blocks at limit)
94
104
  execution-policy record-run Increment agent-run counter (blocks at budget)
95
105
  memory-index rebuild Rebuild SQLite memory index from .specs/ artifacts
106
+ memory-index embed [--force] Optional semantic embeddings (requires config + provider)
96
107
  memory-query --from <id> Bounded context package from the knowledge graph
97
108
  [--depth N] Traversal depth (default 2)
98
109
  [--json] Machine-readable output
99
- memory-search <query> Full-text search over the memory index (FTS5)
110
+ memory-search <query> Full-text search over indexed artifact chunks (FTS5)
100
111
  [--limit N] Max results (default 10)
101
112
  [--json] Machine-readable output
113
+ memory-retrieve "<query>" Hybrid retrieval (FTS + graph + optional semantic)
114
+ [--mode fts|hybrid|semantic] Strategy (default: hybrid)
115
+ [--json] Machine-readable output
102
116
  context-guard status Execute readiness from STATE + tasks.md
103
117
  [--json] Machine-readable output
104
118
  context-guard check-edit <path> Contextual guard before editing a file
@@ -108,6 +122,22 @@ Commands:
108
122
  context-guard check-complete Contextual guard before claiming feature done
109
123
  [feature] Feature id (default: active in STATE)
110
124
  [--json] Machine-readable output
125
+ solution-explore init <feature> Start solution exploration from approved spec
126
+ --candidates A,B[,C] Candidate ids (required, at least two)
127
+ [--labels "a,b,c"] Optional labels aligned to candidate ids
128
+ [--base-ref HEAD] Base ref for candidate worktrees
129
+ [--force] Replace existing exploration.md
130
+ [--json] Machine-readable output
131
+ solution-explore status [feature] Show exploration candidates and decision state
132
+ [--json] Machine-readable output
133
+ solution-explore validate [feature] Gate: comparison matrix complete before select
134
+ [--json] Machine-readable output
135
+ solution-explore select <feature> Record exploration decision
136
+ --candidate A Selected candidate id (required)
137
+ [--merge B] Optional secondary candidate to merge from
138
+ --rationale "…" Why this candidate won (required)
139
+ [--cleanup] Remove non-selected candidate worktrees
140
+ [--json] Machine-readable output
111
141
  validate-spec [spec.md|feature] Closure gate for a feature spec
112
142
  analyze-artifacts [feature] Cross-artifact consistency before task approval
113
143
  validate-tasks [tasks.md|feature] Granularity gate for a task breakdown
@@ -661,6 +691,141 @@ if (command === "--version" || command === "-v" || command === "version") {
661
691
  console.error(`❌ ${err.message}`);
662
692
  process.exit(1);
663
693
  }
694
+ } else if (command === "solution-explore") {
695
+ try {
696
+ const sub = args[0];
697
+ let json = false;
698
+ const rest = [];
699
+
700
+ for (let i = 1; i < args.length; i++) {
701
+ if (args[i] === "--json") {
702
+ json = true;
703
+ } else {
704
+ rest.push(args[i]);
705
+ }
706
+ }
707
+
708
+ const cwd = process.cwd();
709
+
710
+ if (sub === "init") {
711
+ let candidatesRaw = "";
712
+ let labelsRaw = "";
713
+ let baseRef = "HEAD";
714
+ let force = false;
715
+ const positional = [];
716
+
717
+ for (let i = 0; i < rest.length; i++) {
718
+ const arg = rest[i];
719
+ if (arg === "--candidates") {
720
+ candidatesRaw = rest[++i] ?? "";
721
+ } else if (arg === "--labels") {
722
+ labelsRaw = rest[++i] ?? "";
723
+ } else if (arg === "--base-ref") {
724
+ baseRef = rest[++i];
725
+ if (!baseRef) {
726
+ throw new Error("--base-ref requires a git ref");
727
+ }
728
+ } else if (arg === "--force") {
729
+ force = true;
730
+ } else {
731
+ positional.push(arg);
732
+ }
733
+ }
734
+
735
+ const featureId = positional[0];
736
+ if (!featureId || !candidatesRaw) {
737
+ throw new Error(
738
+ "Usage: solution-explore init <feature> --candidates A,B [--labels \"a,b\"] [--base-ref HEAD] [--force]",
739
+ );
740
+ }
741
+
742
+ const ids = candidatesRaw.split(",").map((item) => item.trim()).filter(Boolean);
743
+ const labels = labelsRaw
744
+ ? labelsRaw.split(",").map((item) => item.trim())
745
+ : [];
746
+ const candidateSpecs = ids.map((id, index) => ({
747
+ id: normalizeCandidateId(id),
748
+ label: labels[index] || undefined,
749
+ }));
750
+
751
+ const result = await initExploration(cwd, featureId, candidateSpecs, { baseRef, force });
752
+ process.stdout.write(formatExplorationInit(result, { json }));
753
+
754
+ if (result.workspaces.some((item) => item.status === "failed")) {
755
+ process.exit(1);
756
+ }
757
+ } else if (sub === "status") {
758
+ const featureId = rest[0];
759
+ const status = await getExplorationStatus(cwd, featureId);
760
+ process.stdout.write(formatExplorationStatus(status, { json }));
761
+ } else if (sub === "validate") {
762
+ const featureId = rest[0];
763
+ if (!featureId) {
764
+ throw new Error("Usage: solution-explore validate <feature>");
765
+ }
766
+ const result = await validateExplorationArtifact(cwd, featureId);
767
+ process.stdout.write(formatExplorationValidation(result, { json }));
768
+ if (!result.ok) {
769
+ process.exit(1);
770
+ }
771
+ } else if (sub === "select") {
772
+ let selected = "";
773
+ let mergedFrom = "";
774
+ let rationale = "";
775
+ let cleanup = false;
776
+ const positional = [];
777
+
778
+ for (let i = 0; i < rest.length; i++) {
779
+ const arg = rest[i];
780
+ if (arg === "--candidate") {
781
+ selected = rest[++i] ?? "";
782
+ } else if (arg === "--merge") {
783
+ mergedFrom = rest[++i] ?? "";
784
+ } else if (arg === "--rationale") {
785
+ rationale = rest[++i] ?? "";
786
+ } else if (arg === "--cleanup") {
787
+ cleanup = true;
788
+ } else {
789
+ positional.push(arg);
790
+ }
791
+ }
792
+
793
+ const featureId = positional[0];
794
+ if (!featureId || !selected || !rationale) {
795
+ throw new Error(
796
+ 'Usage: solution-explore select <feature> --candidate A --rationale "…" [--merge B] [--cleanup]',
797
+ );
798
+ }
799
+
800
+ const result = await recordExplorationDecision(cwd, featureId, {
801
+ selected,
802
+ mergedFrom: mergedFrom || null,
803
+ rationale,
804
+ cleanup,
805
+ });
806
+
807
+ if (json) {
808
+ console.log(JSON.stringify(result, null, 2));
809
+ } else {
810
+ console.log(`Recorded decision for ${result.featureId}: ${result.selected}`);
811
+ if (result.mergedFrom) {
812
+ console.log(` Merged from: ${result.mergedFrom}`);
813
+ }
814
+ if (result.cleanup?.length) {
815
+ for (const item of result.cleanup) {
816
+ console.log(` Cleanup ${path.basename(item.path)}: ${item.status}`);
817
+ }
818
+ }
819
+ }
820
+ } else {
821
+ throw new Error(
822
+ "Usage: solution-explore init | status | validate | select",
823
+ );
824
+ }
825
+ } catch (err) {
826
+ console.error(`❌ ${err.message}`);
827
+ process.exit(1);
828
+ }
664
829
  } else if (command === "classify-change") {
665
830
  try {
666
831
  let json = false;
package/lib/constants.js CHANGED
@@ -77,6 +77,7 @@ export const SKILL_ASSETS = [
77
77
  /** @type {{ file: string, remotePath: string }[]} */
78
78
  export const REFERENCE_ASSETS = [
79
79
  { file: "explore.md", remotePath: "skills/references/explore.md" },
80
+ { file: "solution-exploration.md", remotePath: "skills/references/solution-exploration.md" },
80
81
  { file: "project-init.md", remotePath: "skills/references/project-init.md" },
81
82
  { file: "constitution.md", remotePath: "skills/references/constitution.md" },
82
83
  { file: "specify.md", remotePath: "skills/references/specify.md" },
@@ -111,6 +112,9 @@ export const SCRIPT_ASSETS = [
111
112
  { file: "memory_index.py", remotePath: "scripts/memory_index.py" },
112
113
  { file: "memory_query.py", remotePath: "scripts/memory_query.py" },
113
114
  { file: "memory_search.py", remotePath: "scripts/memory_search.py" },
115
+ { file: "memory_retrieve.py", remotePath: "scripts/memory_retrieve.py" },
116
+ { file: "_memory_config.py", remotePath: "scripts/_memory_config.py" },
117
+ { file: "_memory_embed.py", remotePath: "scripts/_memory_embed.py" },
114
118
  ];
115
119
 
116
120
  /** @type {{ file: string, remotePath: string }[]} */
package/lib/gates.js CHANGED
@@ -44,6 +44,7 @@ const AUX_SCRIPTS = {
44
44
  "memory-index": "memory_index.py",
45
45
  "memory-query": "memory_query.py",
46
46
  "memory-search": "memory_search.py",
47
+ "memory-retrieve": "memory_retrieve.py",
47
48
  };
48
49
 
49
50
  const GUARDRAILS_SCRIPTS = { ...GATE_SCRIPTS, ...AUX_SCRIPTS };
@@ -16,6 +16,42 @@ export async function rebuildMemoryIndex(options = {}) {
16
16
  return runGuardrailsScript("memory-index", ["rebuild"], { cwd });
17
17
  }
18
18
 
19
+ /**
20
+ * Build optional semantic embeddings for indexed chunks.
21
+ *
22
+ * @param {{ force?: boolean, json?: boolean, cwd?: string }} [options]
23
+ * @returns {Promise<number>} exit code
24
+ */
25
+ export async function embedMemoryIndex(options = {}) {
26
+ const cwd = options.cwd ?? process.cwd();
27
+ const args = ["embed"];
28
+ if (options.force) {
29
+ args.push("--force");
30
+ }
31
+ if (options.json) {
32
+ args.push("--json");
33
+ }
34
+ return runGuardrailsScript("memory-index", args, { cwd });
35
+ }
36
+
37
+ /**
38
+ * Hybrid retrieval over indexed artifacts.
39
+ *
40
+ * @param {{ query: string, mode?: string, json?: boolean, cwd?: string }} options
41
+ * @returns {Promise<number>} exit code
42
+ */
43
+ export async function retrieveMemory(options) {
44
+ const cwd = options.cwd ?? process.cwd();
45
+ const args = [options.query];
46
+ if (options.mode) {
47
+ args.push("--mode", options.mode);
48
+ }
49
+ if (options.json) {
50
+ args.push("--json");
51
+ }
52
+ return runGuardrailsScript("memory-retrieve", args, { cwd });
53
+ }
54
+
19
55
  /**
20
56
  * Query the knowledge graph for a bounded context package.
21
57
  *