Files
gstack/bin/gstack-decision-search
T
Garry Tan 55e7ed9fec fix: pre-landing review fixes (datamark, DRY, compact, coverage)
Addresses the pre-landing review findings (all INFORMATIONAL, no criticals):
- security: datamark resurfaced decision text at the render boundary
  (lib/gstack-decision.ts datamark() — neutralizes code fences, --- banners,
  <|role|>/</system> markers, control chars, newlines). Applied in
  gstack-decision-search human output so stored text can't masquerade as
  instructions in Context Recovery (codex hardening #3 / AC #7). --json stays raw.
- DRY: extract resolveSlug/gitBranch/flagValue to lib/bin-context.ts; both
  decision bins use it instead of duplicating the helpers.
- compact(): batch the archive append (one write, not N) and shrink the
  mid-compact crash window; simplify the opaque branch/issue ternary.
- coverage: learnings-log injection rejection (D2A wiring), search --recent/
  --scope + NaN-safe --recent, datamark-applied, unparseable lock body,
  compact-empty, corrupt-snapshot degrade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:17:44 -07:00

102 lines
4.1 KiB
TypeScript
Executable File

#!/usr/bin/env bun
/**
* gstack-decision-search — read active decisions (the curated "what did we decide" view).
*
* Usage:
* gstack-decision-search [--query KW] [--scope repo|branch|issue]
* [--branch B] [--issue I] [--recent N] [--all] [--json]
* [--semantic]
*
* Reads the BOUNDED active snapshot (decisions.active.json) — O(active), not a full
* history scan — and rebuilds it from the event log if missing. Scope-filtered to the
* current branch/issue context (recency != relevance). NON-INTERACTIVE. `--all` shows
* superseded decisions too (from the full log). Exit 0 silently when there are none.
*
* `--semantic` (with `--query`) appends an OPTIONAL "related from memory" block from
* gbrain semantic recall. It is a pure enhancement: when gbrain is off/unconfigured/
* empty it degrades silently to the reliable file results above. The reliable path
* never loads gbrain code (the semantic module is imported lazily only here).
*/
import { existsSync } from "fs";
import {
decisionPaths,
readSnapshot,
rebuildSnapshot,
readEvents,
filterByScope,
datamark,
type ActiveDecision,
} from "../lib/gstack-decision";
import { resolveSlug, gitBranch, flagValue } from "../lib/bin-context";
const HERE = import.meta.dir;
const args = process.argv.slice(2);
const slug = resolveSlug(`${HERE}/gstack-slug`);
const paths = decisionPaths(slug);
const queryRaw = flagValue(args, "--query");
const query = queryRaw?.toLowerCase();
const scope = flagValue(args, "--scope");
const branch = flagValue(args, "--branch") ?? gitBranch();
const issue = flagValue(args, "--issue");
const recentRaw = flagValue(args, "--recent");
const recent = recentRaw ? parseInt(recentRaw, 10) : undefined;
const showAll = args.includes("--all");
const asJson = args.includes("--json");
const semantic = args.includes("--semantic");
let rows: ActiveDecision[];
if (showAll) {
// include superseded: every decide event from the full log (no active filter)
rows = readEvents(paths).filter((e): e is ActiveDecision => e.kind === "decide");
} else {
rows = readSnapshot(paths);
// Rebuild only when a snapshot is absent but a log exists (don't write a snapshot
// into a nonexistent store on an empty read — just return nothing).
if (!rows.length && existsSync(paths.log)) rows = rebuildSnapshot(paths);
}
rows = filterByScope(rows, { branch, issue });
if (scope) rows = rows.filter((d) => d.scope === scope);
if (query) {
rows = rows.filter((d) =>
[d.decision, d.rationale, d.alternatives_considered]
.filter((s): s is string => typeof s === "string")
.some((s) => s.toLowerCase().includes(query)),
);
}
rows.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); // newest first
if (recent && recent > 0) rows = rows.slice(0, recent);
if (asJson) {
// --json stays reliable-only (semantic recall is a human-facing supplement).
console.log(JSON.stringify(rows));
process.exit(0);
}
for (const d of rows) {
// Datamark all stored free-text (decision, rationale, branch/issue) — it lands in
// agent context via Context Recovery, so treat it as DATA, not instructions.
const branchTag = d.branch ? `:${datamark(d.branch)}` : "";
const issueTag = d.issue ? `:${datamark(d.issue)}` : "";
const scopeTag = d.scope === "repo" ? "" : ` [${d.scope}${branchTag}${issueTag}]`;
console.log(`- ${datamark(d.decision ?? "")}${scopeTag} (${d.source}, ${d.date.slice(0, 10)})`);
if (d.rationale) console.log(` why: ${datamark(d.rationale)}`);
}
// OPTIONAL gbrain enhancement. Lazy import so the reliable path above never loads
// gbrain code. Degrades silently: null (gbrain off) or [] (nothing found) leaves the
// reliable results above as the answer.
if (semantic && queryRaw) {
const { semanticRecall } = await import("../lib/gstack-decision-semantic");
const hits = semanticRecall(queryRaw);
if (hits && hits.length) {
console.log("\nRelated from memory (gbrain semantic recall):");
for (const h of hits) {
const snip = h.snippet.length > 100 ? `${h.snippet.slice(0, 100)}` : h.snippet;
console.log(` [${h.score.toFixed(2)}] ${h.slug}: ${snip}`);
}
}
}