mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
fix(artifacts): sync the decision store, which no allowlist glob matched
gstack-decision-log enqueues projects/<slug>/decisions.jsonl after every write, but none of the 16 managed globs matched it, so compute_paths_to_stage rejected every one at its "must match at least one allowlist glob" check. The writer and the syncer disagreed silently: enabling artifacts sync backed up learnings, plans, designs and timelines -- everything except the durable decision ledger -- and nothing reported a miss, because a dropped path prints exactly what a synced one does when the queue is otherwise empty. Add the three decisions.* globs and class them artifact so they also sync in artifacts-only mode. The test reads the heredocs out of the script rather than executing it: gstack-artifacts-init.test.ts drives the real script through #!/bin/bash shims and a colon-separated PATH, so it cannot run on Windows -- the platform where the companion slug bug bit.
This commit is contained in:
@@ -291,6 +291,14 @@ projects/*/*-design-*.md
|
||||
projects/*/*-test-plan-*.md
|
||||
projects/*/*-eng-review-test-plan-*.md
|
||||
projects/*/timeline.jsonl
|
||||
# The decision store. gstack-decision-log enqueues projects/<slug>/decisions.jsonl
|
||||
# after EVERY write, but no glob above matched it, so compute_paths_to_stage rejected
|
||||
# all of them at its "must match at least one allowlist glob" check -- a writer
|
||||
# enqueueing a path the syncer is guaranteed to drop. Without these the durable
|
||||
# decision ledger never leaves the machine, on any platform.
|
||||
projects/*/decisions.jsonl
|
||||
projects/*/decisions.active.json
|
||||
projects/*/decisions.archive.jsonl
|
||||
retros/*.md
|
||||
developer-profile.json
|
||||
builder-journey.md
|
||||
@@ -318,6 +326,9 @@ cat > "$GSTACK_HOME/.brain-privacy-map.json" <<'EOF'
|
||||
{"pattern": "projects/*/*-design-*.md", "class": "artifact"},
|
||||
{"pattern": "projects/*/*-test-plan-*.md", "class": "artifact"},
|
||||
{"pattern": "projects/*/*-eng-review-test-plan-*.md", "class": "artifact"},
|
||||
{"pattern": "projects/*/decisions.jsonl", "class": "artifact"},
|
||||
{"pattern": "projects/*/decisions.active.json", "class": "artifact"},
|
||||
{"pattern": "projects/*/decisions.archive.jsonl", "class": "artifact"},
|
||||
{"pattern": "retros/*.md", "class": "artifact"},
|
||||
{"pattern": "builder-journey.md", "class": "artifact"},
|
||||
{"pattern": "projects/*/timeline.jsonl", "class": "behavioral"},
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, "..");
|
||||
const INIT = fs.readFileSync(path.join(ROOT, "bin", "gstack-artifacts-init"), "utf-8");
|
||||
|
||||
/** Pull a quoted heredoc body out of gstack-artifacts-init by target filename. */
|
||||
function heredoc(target: string): string {
|
||||
const re = new RegExp(`cat > "\\$GSTACK_HOME/${target}" <<'EOF'\\n([\\s\\S]*?)\\nEOF\\n`);
|
||||
const m = INIT.match(re);
|
||||
if (!m) throw new Error(`heredoc for ${target} not found in gstack-artifacts-init`);
|
||||
return m[1];
|
||||
}
|
||||
|
||||
/** fnmatch.fnmatchcase semantics, as compute_paths_to_stage applies them:
|
||||
* `*` does not cross a path separator. */
|
||||
function globToRe(g: string): RegExp {
|
||||
return new RegExp("^" + g.split("*").map((s) => s.replace(/[.]/g, "[.]")).join("[^/]*") + "$");
|
||||
}
|
||||
|
||||
const DECISION_PATHS = [
|
||||
"projects/acme-widget/decisions.jsonl",
|
||||
"projects/acme-widget/decisions.active.json",
|
||||
"projects/acme-widget/decisions.archive.jsonl",
|
||||
];
|
||||
|
||||
/**
|
||||
* gstack-decision-log:40 enqueues projects/<slug>/decisions.jsonl after EVERY write,
|
||||
* but no managed glob matched it, so compute_paths_to_stage rejected all of them at
|
||||
* its "must match at least one allowlist glob" check. The writer and the syncer
|
||||
* disagreed silently: turning artifacts sync on backed up learnings, plans, designs
|
||||
* and timelines -- everything EXCEPT the durable decision ledger -- and nothing
|
||||
* anywhere reported a miss, because a dropped path prints exactly what a synced one
|
||||
* does when the queue is otherwise empty.
|
||||
*
|
||||
* Source-level rather than end-to-end: gstack-artifacts-init.test.ts drives the real
|
||||
* script through #!/bin/bash shims and a colon-separated PATH, so it cannot run on
|
||||
* Windows -- which is the platform where this bug bit.
|
||||
*/
|
||||
describe("the artifacts allowlist covers the decision store", () => {
|
||||
const globs = heredoc("\\.brain-allowlist")
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && !l.startsWith("#"));
|
||||
|
||||
test("every decisions.* path matches at least one allowlist glob", () => {
|
||||
for (const p of DECISION_PATHS) {
|
||||
expect({ p, matched: globs.some((g) => globToRe(g).test(p)) }).toEqual({ p, matched: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("decisions.* are class artifact, so they sync in artifacts-only mode too", () => {
|
||||
const map = JSON.parse(heredoc("\\.brain-privacy-map\\.json"));
|
||||
for (const p of DECISION_PATHS) {
|
||||
const hit = map.find((e: { pattern: string; class: string }) => globToRe(e.pattern).test(p));
|
||||
expect({ p, cls: hit?.class }).toEqual({ p, cls: "artifact" });
|
||||
}
|
||||
});
|
||||
|
||||
test("the allowlist still ends with the user-additions marker", () => {
|
||||
// Additions below it survive re-init; a glob added above would be silently
|
||||
// overwritten the next time gstack-artifacts-init runs.
|
||||
expect(heredoc("\\.brain-allowlist").trimEnd()).toMatch(/# ---- USER ADDITIONS BELOW ----/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user