mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 06:28:59 +02:00
fix(design): print and alternate stylesheets are not scanned as page CSS; no cross-run dump sweep; probe-state and design-system caveats in prose
- The dump inlined every linked sheet's rules as active CSS, so a print sheet's 12pt black text or an alternate theme produced tiny-text and palette findings the user never sees; disabled and alternate sheets are skipped and a media-scoped sheet is wrapped in its @media block. - The cross-run dump sweep is gone: two same-day reviews shared REPORT_DIR and one run's sweep deleted the other's dumps mid-audit. Dumps stay per run, owner-only, deleted after Phase 9 unless --keep-dom (now defined in the prose), and an interrupted run's dumps wait for the user. - Prose: design-system-* rows in DOM mode compare the page to THIS repo's DESIGN.md and apply only to the repo's own app; an empty scan JSON with exit 0 means the probe state changed since Setup (read stderr); the persist block names a missing dump instead of mislabeling it as a redaction block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
69b0856d42
commit
3f207247a8
@@ -819,7 +819,7 @@ Remember `RUN_ID` and restate it literally in later blocks (each bash block is a
|
||||
_DJ=$(mktemp); bun --no-env-file run $HOME/.claude/skills/gstack/bin/gstack-design-detect.ts scan --changed <base> --format gstack --host claude > "$_DJ"; echo "DETECT_EXIT_CODE=$?"; echo "DETECT_JSON=$_DJ"
|
||||
```
|
||||
|
||||
DOM mode never scans source (Rule 4): Phase 3 dumps each page's rendered DOM into `$REPORT_DIR/dom/$RUN_ID/` and scans once after the last page. Exit 2 means findings; exit 1 means a target could not be scanned (note which, move on); exit 3 is a gstack bug (`DESIGN_DETECT_INTERNAL_ERROR`: report it, never retry). Each rule in the `DETECT_TOP` block becomes one `FINDING-NNN` tagged `[rule-id]` with the printed impact and its location list, never one finding per hit. A detector hit is evidence, not a verdict: confirm it in the rendered page before it counts, drop it when DESIGN.md tokens bless the value, never pad the report with advisory rows. Phase 9 recomputes the same way (DOM mode re-dumps the affected pages after reload; source mode rescans the touched files) and Phase 10 reports `Detector: N → M`. When `IMPECCABLE_SKILL: present`, end each deferred finding with the `handoff=` command the scan printed (`/impeccable typeset`, `layout`, `colorize`, `harden`, `clarify`, `polish`, `animate`, `quieter`); recommend it, never open its files.
|
||||
DOM mode never scans source (Rule 4): Phase 3 dumps each page's rendered DOM into `$REPORT_DIR/dom/$RUN_ID/` and scans once after the last page. Exit 2 means findings; exit 1 means a target could not be scanned (note which, move on); exit 0 with an empty `$_DJ` means the probe state changed since Setup (read the sentinel on stderr); exit 3 is a gstack bug (`DESIGN_DETECT_INTERNAL_ERROR`: report it, never retry). Each rule in the `DETECT_TOP` block becomes one `FINDING-NNN` tagged `[rule-id]` with the printed impact and its location list, never one finding per hit. A detector hit is evidence, not a verdict: confirm it in the rendered page before it counts, drop it when DESIGN.md tokens bless the value, never pad the report with advisory rows. Phase 9 recomputes the same way (DOM mode re-dumps the affected pages after reload; source mode rescans the touched files) and Phase 10 reports `Detector: N → M`. When `IMPECCABLE_SKILL: present`, end each deferred finding with the `handoff=` command the scan printed (`/impeccable typeset`, `layout`, `colorize`, `harden`, `clarify`, `polish`, `animate`, `quieter`); recommend it, never open its files.
|
||||
|
||||
---
|
||||
|
||||
@@ -1093,11 +1093,12 @@ _TMP=$(mktemp -d); _DUMP=$(cat "$HOME/.claude/skills/gstack/lib/dom-dump.js")
|
||||
$B js '('"$_DUMP"')()' --out "$_TMP/{page}.dom.html" --raw && echo "DUMP=$_TMP/{page}.dom.html"
|
||||
```
|
||||
|
||||
Persist it into this run's directory, size-capped and redaction-checked: a HIGH finding, or a redaction tool that fails to run, skips the page, not the review; MEDIUM findings (emails, PII shapes on an authenticated page) persist owner-only (mode 600) and are deleted with the rest after Phase 9. Before the first dump of a run, remove earlier runs' dumps (`find "<REPORT_DIR from Setup>/dom" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + 2>/dev/null`) unless the user passed `--keep-dom` this run. Each bash block is a fresh shell: restate the report directory and run id from Setup literally.
|
||||
Persist it into this run's directory, size-capped and redaction-checked: a HIGH finding, or a redaction tool that fails to run, skips the page, not the review; MEDIUM findings (emails, PII shapes on an authenticated page) persist owner-only (mode 600) and are deleted with the rest after Phase 9 (`--keep-dom`, a design-review flag, keeps them; an interrupted run's dumps stay owner-only under their run id until you delete them). Each bash block is a fresh shell: restate the report directory and run id from Setup literally.
|
||||
|
||||
```bash
|
||||
_D="<ASIDE_DIR or $_TMP>/{page}.dom.html"; _REPORT="<REPORT_DIR from Setup>"; _RUN="<RUN_ID from Setup>"
|
||||
if [ "$(wc -c < "$_D")" -gt 10485760 ]; then echo "DOM_DUMP_TOO_LARGE: {page} $(wc -c < "$_D")"; rm -f "$_D"
|
||||
if [ ! -s "$_D" ]; then echo "DOM_DUMP_MISSING: {page} (the dump script wrote nothing)"
|
||||
elif [ "$(wc -c < "$_D")" -gt 10485760 ]; then echo "DOM_DUMP_TOO_LARGE: {page} $(wc -c < "$_D")"; rm -f "$_D"
|
||||
elif $HOME/.claude/skills/gstack/bin/gstack-redact --from-file "$_D" --max-bytes 10485760 >/dev/null 2>&1; _RC=$?; [ "$_RC" -ne 0 ] && [ "$_RC" -ne 2 ]; then echo "DOM_DUMP_REDACTION_BLOCKED: {page} redact-exit=$_RC"; rm -f "$_D"
|
||||
else mkdir -p "$_REPORT/dom/$_RUN" && cp "$_D" "$_REPORT/dom/$_RUN/" && chmod 600 "$_REPORT/dom/$_RUN/{page}.dom.html" && rm -f "$_D" && echo "DOM_DUMP_OK: {page}"; fi
|
||||
```
|
||||
@@ -1108,7 +1109,7 @@ After the LAST page's dump, scan the run directory once (source mode scanned in
|
||||
_DJ=$(mktemp); bun --no-env-file run $HOME/.claude/skills/gstack/bin/gstack-design-detect.ts scan --format gstack --host claude "<REPORT_DIR from Setup>/dom/<RUN_ID>" > "$_DJ"; echo "DETECT_EXIT_CODE=$?"; echo "DETECT_JSON=$_DJ"
|
||||
```
|
||||
|
||||
Say once in the report: "static scan of the rendered DOM; cross-origin CSS not resolved". A DOM-mode `file:line` points into `{page}.dom.html` and is approximate (HTML findings carry line 0); the `snippet` locates the element. Confirm each hit in the rendered page, never by hunting a source line. Dumps are deleted after Phase 9 unless the user passed `--keep-dom`.
|
||||
Say once in the report: "static scan of the rendered DOM; cross-origin CSS not resolved". A DOM-mode `file:line` points into `{page}.dom.html` and is approximate (HTML findings carry line 0); the `snippet` locates the element. Confirm each hit in the rendered page, never by hunting a source line. `design-system-*` rows compare the page against THIS repository's DESIGN.md: keep them only when the page is this repository's own app. An empty `$_DJ` with exit 0 means the probe state changed since Setup: read the sentinel the scan printed on stderr. Dumps are deleted after Phase 9 unless the user passed `--keep-dom`.
|
||||
|
||||
### Auth Detection
|
||||
|
||||
|
||||
@@ -50,8 +50,14 @@ export const DOM_DUMP_SCRIPT = String.raw`() => {
|
||||
liveLinks.forEach((link, i) => {
|
||||
const sheet = link.sheet;
|
||||
if (!sheet) return;
|
||||
if (link.disabled || (link.getAttribute("rel") || "").indexOf("alternate") !== -1) {
|
||||
if (cloneLinks[i]) cloneLinks[i].remove(); // not active CSS: never scanned as page styles
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = Array.from(sheet.cssRules).map((rule) => rule.cssText).join("\n");
|
||||
let text = Array.from(sheet.cssRules).map((rule) => rule.cssText).join("\n");
|
||||
const media = sheet.media && sheet.media.mediaText;
|
||||
if (media && media !== "all") text = "@media " + media + " {\n" + text + "\n}"; // a print sheet stays a print sheet
|
||||
inlined.push("/* gstack-dom-dump: " + (sheet.href || "link") + " */\n" + text);
|
||||
if (cloneLinks[i]) cloneLinks[i].remove();
|
||||
} catch (err) {
|
||||
|
||||
+7
-1
@@ -8,8 +8,14 @@
|
||||
liveLinks.forEach((link, i) => {
|
||||
const sheet = link.sheet;
|
||||
if (!sheet) return;
|
||||
if (link.disabled || (link.getAttribute("rel") || "").indexOf("alternate") !== -1) {
|
||||
if (cloneLinks[i]) cloneLinks[i].remove(); // not active CSS: never scanned as page styles
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = Array.from(sheet.cssRules).map((rule) => rule.cssText).join("\n");
|
||||
let text = Array.from(sheet.cssRules).map((rule) => rule.cssText).join("\n");
|
||||
const media = sheet.media && sheet.media.mediaText;
|
||||
if (media && media !== "all") text = "@media " + media + " {\n" + text + "\n}"; // a print sheet stays a print sheet
|
||||
inlined.push("/* gstack-dom-dump: " + (sheet.href || "link") + " */\n" + text);
|
||||
if (cloneLinks[i]) cloneLinks[i].remove();
|
||||
} catch (err) {
|
||||
|
||||
@@ -239,11 +239,12 @@ _TMP=$(mktemp -d); _DUMP=$(cat "${toShellPath(ctx.paths.skillRoot)}/${DOM_DUMP_F
|
||||
$B js '('"$_DUMP"')()' --out "$_TMP/{page}.dom.html" --raw && echo "DUMP=$_TMP/{page}.dom.html"
|
||||
\`\`\`
|
||||
|
||||
Persist it into this run's directory, size-capped and redaction-checked: a HIGH finding, or a redaction tool that fails to run, skips the page, not the review; MEDIUM findings (emails, PII shapes on an authenticated page) persist owner-only (mode 600) and are deleted with the rest after Phase 9. Before the first dump of a run, remove earlier runs' dumps (\`find "<REPORT_DIR from Setup>/dom" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + 2>/dev/null\`) unless the user passed \`--keep-dom\` this run. Each bash block is a fresh shell: restate the report directory and run id from Setup literally.
|
||||
Persist it into this run's directory, size-capped and redaction-checked: a HIGH finding, or a redaction tool that fails to run, skips the page, not the review; MEDIUM findings (emails, PII shapes on an authenticated page) persist owner-only (mode 600) and are deleted with the rest after Phase 9 (\`--keep-dom\`, a design-review flag, keeps them; an interrupted run's dumps stay owner-only under their run id until you delete them). Each bash block is a fresh shell: restate the report directory and run id from Setup literally.
|
||||
|
||||
\`\`\`bash
|
||||
_D="<ASIDE_DIR or $_TMP>/{page}.dom.html"; _REPORT="<REPORT_DIR from Setup>"; _RUN="<RUN_ID from Setup>"
|
||||
if [ "$(wc -c < "$_D")" -gt ${DETECT_LIMITS.domDumpBytes} ]; then echo "${SENTINEL.DOM_DUMP_TOO_LARGE}: {page} $(wc -c < "$_D")"; rm -f "$_D"
|
||||
if [ ! -s "$_D" ]; then echo "${SENTINEL.DOM_DUMP_MISSING}: {page} (the dump script wrote nothing)"
|
||||
elif [ "$(wc -c < "$_D")" -gt ${DETECT_LIMITS.domDumpBytes} ]; then echo "${SENTINEL.DOM_DUMP_TOO_LARGE}: {page} $(wc -c < "$_D")"; rm -f "$_D"
|
||||
elif ${toShellPath(ctx.paths.binDir)}/gstack-redact --from-file "$_D" --max-bytes ${DETECT_LIMITS.domDumpBytes} >/dev/null 2>&1; _RC=$?; [ "$_RC" -ne 0 ] && [ "$_RC" -ne 2 ]; then echo "${SENTINEL.DOM_DUMP_REDACTION_BLOCKED}: {page} redact-exit=$_RC"; rm -f "$_D"
|
||||
else mkdir -p "$_REPORT/dom/$_RUN" && cp "$_D" "$_REPORT/dom/$_RUN/" && chmod 600 "$_REPORT/dom/$_RUN/{page}.dom.html" && rm -f "$_D" && echo "${SENTINEL.DOM_DUMP_OK}: {page}"; fi
|
||||
\`\`\`
|
||||
@@ -254,7 +255,7 @@ After the LAST page's dump, scan the run directory once (source mode scanned in
|
||||
_DJ=$(mktemp); bun --no-env-file run ${toShellPath(ctx.paths.binDir)}/gstack-design-detect.ts scan --format gstack --host ${ctx.host} "<REPORT_DIR from Setup>/dom/<RUN_ID>" > "$_DJ"${DETECT_EXIT_ECHO}; echo "${SENTINEL.DETECT_JSON}=$_DJ"
|
||||
\`\`\`
|
||||
|
||||
Say once in the report: "static scan of the rendered DOM; cross-origin CSS not resolved". A DOM-mode \`file:line\` points into \`{page}.dom.html\` and is approximate (HTML findings carry line 0); the \`snippet\` locates the element. Confirm each hit in the rendered page, never by hunting a source line. Dumps are deleted after Phase 9 unless the user passed \`--keep-dom\`.
|
||||
Say once in the report: "static scan of the rendered DOM; cross-origin CSS not resolved". A DOM-mode \`file:line\` points into \`{page}.dom.html\` and is approximate (HTML findings carry line 0); the \`snippet\` locates the element. Confirm each hit in the rendered page, never by hunting a source line. \`design-system-*\` rows compare the page against THIS repository's DESIGN.md: keep them only when the page is this repository's own app. An empty \`$_DJ\` with exit 0 means the probe state changed since Setup: read the sentinel the scan printed on stderr. Dumps are deleted after Phase 9 unless the user passed \`--keep-dom\`.
|
||||
|
||||
### Auth Detection
|
||||
|
||||
@@ -888,7 +889,7 @@ export function generateDesignDetector(ctx: TemplateContext, args?: string[]): s
|
||||
_DJ=$(mktemp); ${bin} scan --changed <base> --format gstack --host ${ctx.host} > "$_DJ"${DETECT_EXIT_ECHO}; echo "${SENTINEL.DETECT_JSON}=$_DJ"
|
||||
\`\`\`
|
||||
|
||||
DOM mode never scans source (Rule 4): Phase 3 dumps each page's rendered DOM into \`$REPORT_DIR/dom/$RUN_ID/\` and scans once after the last page. Exit 2 means findings; exit 1 means a target could not be scanned (note which, move on); exit 3 is a gstack bug (\`${SENTINEL.INTERNAL_ERROR}\`: report it, never retry). Each rule in the \`${SENTINEL.DETECT_TOP}\` block becomes one \`FINDING-NNN\` tagged \`[rule-id]\` with the printed impact and its location list, never one finding per hit. A detector hit is evidence, not a verdict: confirm it in the rendered page before it counts, drop it when DESIGN.md tokens bless the value, never pad the report with advisory rows. Phase 9 recomputes the same way (DOM mode re-dumps the affected pages after reload; source mode rescans the touched files) and Phase 10 reports \`Detector: N → M\`. When \`${SENTINEL.SKILL}: present\`, end each deferred finding with the \`handoff=\` command the scan printed (\`/impeccable ${HANDOFF_COMMANDS.join('\`, \`')}\`); recommend it, never open its files.`;
|
||||
DOM mode never scans source (Rule 4): Phase 3 dumps each page's rendered DOM into \`$REPORT_DIR/dom/$RUN_ID/\` and scans once after the last page. Exit 2 means findings; exit 1 means a target could not be scanned (note which, move on); exit 0 with an empty \`$_DJ\` means the probe state changed since Setup (read the sentinel on stderr); exit 3 is a gstack bug (\`${SENTINEL.INTERNAL_ERROR}\`: report it, never retry). Each rule in the \`${SENTINEL.DETECT_TOP}\` block becomes one \`FINDING-NNN\` tagged \`[rule-id]\` with the printed impact and its location list, never one finding per hit. A detector hit is evidence, not a verdict: confirm it in the rendered page before it counts, drop it when DESIGN.md tokens bless the value, never pad the report with advisory rows. Phase 9 recomputes the same way (DOM mode re-dumps the affected pages after reload; source mode rescans the touched files) and Phase 10 reports \`Detector: N → M\`. When \`${SENTINEL.SKILL}: present\`, end each deferred finding with the \`handoff=\` command the scan printed (\`/impeccable ${HANDOFF_COMMANDS.join('\`, \`')}\`); recommend it, never open its files.`;
|
||||
}
|
||||
if (mode === 'gate') {
|
||||
return `### Slop Gate (bounded, never a loop)
|
||||
|
||||
@@ -41,10 +41,13 @@ describe.skipIf(!BROWSE || !POSIX || !OPTED_IN)('lib/dom-dump.js in a real DOM (
|
||||
});
|
||||
const big = 'data:image/png;base64,' + 'A'.repeat(1500);
|
||||
fs.writeFileSync(path.join(site, 'styles.css'), '.hero { background: linear-gradient(135deg, #6366f1, #8b5cf6); } .x { background-image: url("' + big + '"); } .y { background: url("/y.png?token=SECRETCSS") }\n');
|
||||
fs.writeFileSync(path.join(site, 'print.css'), '.p { font-size: 4px }\n');
|
||||
fs.writeFileSync(path.join(site, 'alt.css'), '.alt { color: #ff00ff }\n');
|
||||
fs.writeFileSync(path.join(site, 'index.html'), `<!DOCTYPE html><html><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width"><meta name="description" content="SECRET DESCRIPTION">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="http://127.0.0.1:1/cross-origin.css">
|
||||
<link rel="stylesheet" media="print" href="print.css"><link rel="alternate stylesheet" href="alt.css" title="alt">
|
||||
<script>window.__x = "SCRIPT BODY";</script></head><body>
|
||||
<input value="SECRET INPUT"><textarea>SECRET TEXT</textarea>
|
||||
<a href="/page?token=SECRET">link</a>
|
||||
@@ -109,6 +112,9 @@ describe.skipIf(!BROWSE || !POSIX || !OPTED_IN)('lib/dom-dump.js in a real DOM (
|
||||
expect(html).toContain('url("/y.png")');
|
||||
expect(html).not.toContain('SECRETSRCDOC');
|
||||
expect(html).not.toContain('SECRETXLINK');
|
||||
expect(html).toMatch(/@media print \{[\s\S]*font-size: 4px[\s\S]*\}/); // a print sheet is scanned as print CSS, not as the page's styles
|
||||
expect(html).not.toContain('#ff00ff'); // an alternate stylesheet is not active CSS
|
||||
expect(html).not.toMatch(/<link[^>]*alt\.css/);
|
||||
expect(html).toContain('srcset="/a.png 1x, /b.png 2x"');
|
||||
expect(html).not.toContain('L'.repeat(40));
|
||||
expect(html).toContain('data-short="ok"');
|
||||
|
||||
Reference in New Issue
Block a user