mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-07 17:28:23 +01:00
feat(diff-scope): template UI in .ts/.js counts as frontend; receipt records the static design pass (GST-1718) (#4112)
* feat(diff-scope): template UI in .ts/.js counts as frontend; receipt records the static design pass (GST-1718) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: regenerate ship goldens for the static design-review line (GST-1718) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Review-Finding: p1/specialist-batched#1 reader=specialist Review-Finding: p1/red-team#1 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: count edits inside tagged templates and spaced template tags as frontend Review-Finding: v1/outside-voice#1 reader=outside-voice Review-Finding: v1/adversarial-native#1 reader=adversarial-native Review-Finding: v1/outside-voice#2 reader=outside-voice Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: one set comparison per template file instead of a grep per changed line Review-Finding: p2/red-team#2 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): GST-1718 fragment covers edits inside tagged templates Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship-policy): design record wording matches the in-template rule Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
40071a4237
commit
5862765a20
20 files changed
+273
-14
No files matched your search
@@ -542,3 +542,134 @@ describe('uncommitted work is visible (#2299)', () => {
|
||||
expect(vars.SCOPE_DOCS).toBe('true'); // pre-fix: git's octal quoting defeated *.md
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-diff-scope template UI (GST-1718)', () => {
|
||||
const LIT_BASE = [
|
||||
"import { html } from 'lit';",
|
||||
'export function rowTpl(name: string) {',
|
||||
' return html`<div class="permission-row"><span class="label">${name}</span></div>`;',
|
||||
'}',
|
||||
'export async function load(): Promise<void> {',
|
||||
' await fetch("/api/personas");',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
function templateRepo(): { dir: string; git: (...a: string[]) => ReturnType<typeof spawnSync> } {
|
||||
const dir = createRepo([]);
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: dir, stdio: 'pipe', timeout: 5000 });
|
||||
git('checkout', '-q', 'main');
|
||||
mkdirSync(join(dir, 'plugins', 'web-ui', 'src'), { recursive: true });
|
||||
mkdirSync(join(dir, 'src', 'personas'), { recursive: true });
|
||||
writeFileSync(join(dir, 'plugins', 'web-ui', 'src', 'personas.ts'), LIT_BASE);
|
||||
writeFileSync(join(dir, 'src', 'personas', 'persona-service.ts'), 'export const svc = 1;\n');
|
||||
git('add', '.'); git('commit', '-qm', 'base screens');
|
||||
git('checkout', '-q', '-b', 'topic');
|
||||
return { dir, git };
|
||||
}
|
||||
const edit = (dir: string, from: string, to: string) => {
|
||||
const p = join(dir, 'plugins', 'web-ui', 'src', 'personas.ts');
|
||||
writeFileSync(p, readFileSync(p, 'utf8').replace(from, to));
|
||||
};
|
||||
|
||||
// Value: protects=a screen written as a Lit template in a .ts file reads as frontend, so /ship's design-lite check and
|
||||
// the design specialist run (qm#3186 read SCOPE_FRONTEND=false and both skipped silently); fails_when=the content arm
|
||||
// is missing, or it clears SCOPE_BACKEND for the same file; seam=a synthetic Lit file at qm#3186's path, committed and
|
||||
// read through --head and through the working tree; no_claim=no qm source is used.
|
||||
test('a markup edit in a Lit template file is frontend and still backend, on --head and in the working tree', () => {
|
||||
const { dir, git } = templateRepo();
|
||||
edit(dir, '<span class="label">${name}</span>', '<span class="label">${name}</span><small class="note">Only owner</small>');
|
||||
expect(runScope(dir).SCOPE_FRONTEND).toBe('true');
|
||||
writeFileSync(join(dir, 'src', 'personas', 'persona-service.ts'), 'export const svc = 2;\n');
|
||||
git('add', '.'); git('commit', '-qm', 'persona access');
|
||||
const head = git('rev-parse', 'HEAD').stdout.toString().trim();
|
||||
const out = spawnSync('bash', [SCRIPT, 'main', '--head', head], { cwd: dir, stdio: 'pipe', timeout: 5000 }).stdout.toString();
|
||||
expect(out).toContain('SCOPE_FRONTEND=true');
|
||||
expect(out).toContain('SCOPE_BACKEND=true');
|
||||
});
|
||||
|
||||
// Value: protects=logic edits in a file that happens to hold templates, and TypeScript generics, stay backend-only, so
|
||||
// the design reviewers do not fire on every touch of a UI module; fails_when=the arm counts any changed line in a
|
||||
// template file, or reads Promise<void> as a tag; seam=none
|
||||
test('a logic-only or generic-only edit in a template file is not frontend', () => {
|
||||
const { dir } = templateRepo();
|
||||
edit(dir, 'await fetch("/api/personas");', 'await fetch("/api/personas?v=2");');
|
||||
expect(runScope(dir).SCOPE_FRONTEND).toBe('false');
|
||||
edit(dir, 'Promise<void>', 'Promise<Map<string, number>>');
|
||||
const vars = runScope(dir);
|
||||
expect(vars.SCOPE_FRONTEND).toBe('false');
|
||||
expect(vars.SCOPE_BACKEND).toBe('true');
|
||||
});
|
||||
|
||||
// Value: protects=test fixtures that render templates do not turn on the design reviewers, while a brand-new or a
|
||||
// deleted screen does; fails_when=test paths are scanned, untracked files are ignored, or a deleted template file
|
||||
// reads as no change; seam=none
|
||||
test('a template test file is not frontend; an untracked or deleted template file is', () => {
|
||||
const t = templateRepo();
|
||||
mkdirSync(join(t.dir, 'plugins', 'web-ui', 'test'), { recursive: true });
|
||||
writeFileSync(join(t.dir, 'plugins', 'web-ui', 'test', 'personas-page.test.ts'), LIT_BASE);
|
||||
writeFileSync(join(t.dir, 'plugins', 'web-ui', 'src', 'row.test.ts'), LIT_BASE);
|
||||
expect(runScope(t.dir).SCOPE_FRONTEND).toBe('false');
|
||||
writeFileSync(join(t.dir, 'plugins', 'web-ui', 'src', 'picker.ts'), LIT_BASE);
|
||||
expect(runScope(t.dir).SCOPE_FRONTEND).toBe('true');
|
||||
const d = templateRepo();
|
||||
d.git('rm', '-q', 'plugins/web-ui/src/personas.ts');
|
||||
d.git('commit', '-qm', 'drop screen');
|
||||
expect(runScope(d.dir).SCOPE_FRONTEND).toBe('true');
|
||||
});
|
||||
|
||||
// Value: protects=a Lit screen with a non-ASCII file name still reads as frontend (git grep octal-quotes such paths
|
||||
// unless -z, and the quoted path matched no pathspec), and a <placeholder> in a comment does not; fails_when=the
|
||||
// template arm reads git grep without -z, or counts comment lines as markup; seam=none
|
||||
test('a non-ASCII template file is frontend on --head and in the working tree; a comment placeholder is not', () => {
|
||||
const { dir, git } = templateRepo();
|
||||
edit(dir, 'export async function load', '// usage: load <base> --head <sha>\nexport async function load');
|
||||
expect(runScope(dir).SCOPE_FRONTEND).toBe('false');
|
||||
git('checkout', '-q', '.');
|
||||
writeFileSync(join(dir, 'plugins', 'web-ui', 'src', 'café.ts'), LIT_BASE);
|
||||
expect(runScope(dir).SCOPE_FRONTEND).toBe('true');
|
||||
git('add', '.'); git('commit', '-qm', 'cafe screen');
|
||||
const head = git('rev-parse', 'HEAD').stdout.toString().trim();
|
||||
const out = spawnSync('bash', [SCRIPT, 'main', '--head', head], { cwd: dir, stdio: 'pipe', timeout: 5000 }).stdout.toString();
|
||||
expect(out).toContain('SCOPE_FRONTEND=true');
|
||||
});
|
||||
|
||||
// Value: protects=a spacing or binding edit inside a multiline Lit template (no tag, element or class= on the changed
|
||||
// line) still reads as frontend, as does a screen tagged with whitespace before the backtick; fails_when=the arm
|
||||
// tests changed lines alone instead of their place inside a tagged template, or the tag regex requires the backtick
|
||||
// right after html/css/svg; seam=none
|
||||
test('a CSS-only or binding-only edit inside a multiline template is frontend; so is html `...` with a space', () => {
|
||||
const { dir, git } = templateRepo();
|
||||
git('checkout', '-q', 'main');
|
||||
const card = join(dir, 'plugins', 'web-ui', 'src', 'card.ts');
|
||||
writeFileSync(card, [
|
||||
"import { LitElement, css, html } from 'lit';",
|
||||
'export class Card extends LitElement {',
|
||||
' static styles = css`',
|
||||
' .row { padding: 4px; }',
|
||||
' `;',
|
||||
' render() {',
|
||||
' return html`',
|
||||
' <div class="row">',
|
||||
' ${this.label}',
|
||||
' </div>`;',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'));
|
||||
git('add', '.'); git('commit', '-qm', 'card');
|
||||
git('checkout', '-q', '-B', 'topic', 'main');
|
||||
const swap = (from: string, to: string) => writeFileSync(card, readFileSync(card, 'utf8').replace(from, to));
|
||||
swap('padding: 4px;', 'padding: 12px;');
|
||||
expect(runScope(dir).SCOPE_FRONTEND).toBe('true');
|
||||
git('commit', '-qam', 'spacing');
|
||||
const head = git('rev-parse', 'HEAD').stdout.toString().trim();
|
||||
const out = spawnSync('bash', [SCRIPT, 'main', '--head', head], { cwd: dir, stdio: 'pipe', timeout: 5000 }).stdout.toString();
|
||||
expect(out).toContain('SCOPE_FRONTEND=true');
|
||||
git('reset', '-q', '--hard', 'HEAD~1');
|
||||
swap('${this.label}', '${this.title}');
|
||||
expect(runScope(dir).SCOPE_FRONTEND).toBe('true');
|
||||
git('checkout', '-q', '.');
|
||||
writeFileSync(join(dir, 'plugins', 'web-ui', 'src', 'row.ts'), "import { html } from 'lit';\nexport const row = html `<p>Hi</p>`;\n");
|
||||
expect(runScope(dir).SCOPE_FRONTEND).toBe('true');
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -425,7 +425,7 @@ VERDICT: {CLEARED or NOT CLEARED} — {reason}
|
||||
An outside review with status `unverified` or `unavailable` is missing coverage, never a pass.
|
||||
|
||||
Over 200 lines, `DIFF:` recommends `/autoplan`. `SCOPE: frontend=true` (from
|
||||
`gstack-diff-scope <base>`) with no design review: mention "Design Review not run — Step 9 includes the lite check; consider /design-review for a full visual audit."
|
||||
`gstack-diff-scope <base>`) (Lit or htm templates in .ts/.js count) with no design review: mention "Design Review: static (no preview declared) — Step 9 runs the lite check only; consider /design-review on a served page for a visual audit."
|
||||
|
||||
Continue to Step 2 without asking; Step 9 applies the review gates.
|
||||
|
||||
|
||||
+1
-1
@@ -424,7 +424,7 @@ VERDICT: {CLEARED or NOT CLEARED} — {reason}
|
||||
An outside review with status `unverified` or `unavailable` is missing coverage, never a pass.
|
||||
|
||||
Over 200 lines, `DIFF:` recommends `/autoplan`. `SCOPE: frontend=true` (from
|
||||
`gstack-diff-scope <base>`) with no design review: mention "Design Review not run — Step 9 includes the lite check; consider /design-review for a full visual audit."
|
||||
`gstack-diff-scope <base>`) (Lit or htm templates in .ts/.js count) with no design review: mention "Design Review: static (no preview declared) — Step 9 runs the lite check only; consider /design-review on a served page for a visual audit."
|
||||
|
||||
Continue to Step 2 without asking; Step 9 applies the review gates.
|
||||
|
||||
|
||||
+1
-1
@@ -424,7 +424,7 @@ VERDICT: {CLEARED or NOT CLEARED} — {reason}
|
||||
An outside review with status `unverified` or `unavailable` is missing coverage, never a pass.
|
||||
|
||||
Over 200 lines, `DIFF:` recommends `/autoplan`. `SCOPE: frontend=true` (from
|
||||
`gstack-diff-scope <base>`) with no design review: mention "Design Review not run — Step 9 includes the lite check; consider /design-review for a full visual audit."
|
||||
`gstack-diff-scope <base>`) (Lit or htm templates in .ts/.js count) with no design review: mention "Design Review: static (no preview declared) — Step 9 runs the lite check only; consider /design-review on a served page for a visual audit."
|
||||
|
||||
Continue to Step 2 without asking; Step 9 applies the review gates.
|
||||
|
||||
|
||||
+1
-1
@@ -426,7 +426,7 @@ VERDICT: {CLEARED or NOT CLEARED} — {reason}
|
||||
An outside review with status `unverified` or `unavailable` is missing coverage, never a pass.
|
||||
|
||||
Over 200 lines, `DIFF:` recommends `/autoplan`. `SCOPE: frontend=true` (from
|
||||
`gstack-diff-scope <base>`) with no design review: mention "Design Review not run — Step 9 includes the lite check; consider /design-review for a full visual audit."
|
||||
`gstack-diff-scope <base>`) (Lit or htm templates in .ts/.js count) with no design review: mention "Design Review: static (no preview declared) — Step 9 runs the lite check only; consider /design-review on a served page for a visual audit."
|
||||
|
||||
Continue to Step 2 without asking; Step 9 applies the review gates.
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ describe('write records the review depth at the checked-out head', () => {
|
||||
expect(r.review_depth).toMatchObject({ depth: 'light', class: 'code', base, registry: base, gate_scan: { tagged: 0, head } });
|
||||
expect(r.review_depth!.runtime_lines).toBeGreaterThanOrEqual(5);
|
||||
expect(r.review_depth!.runtime_lines).toBeLessThanOrEqual(50);
|
||||
expect(r.design).toBeUndefined();
|
||||
const other = path.join(repo.root, 'other.md');
|
||||
const bad = writeReceipt(repo, other, ['--head', older]);
|
||||
expect(bad.status).toBe(EXIT.fail);
|
||||
@@ -60,6 +61,18 @@ describe('write records the review depth at the checked-out head', () => {
|
||||
expect(fs.existsSync(other)).toBe(false);
|
||||
}, 60_000);
|
||||
|
||||
// Value: protects=a rendered-UI diff's receipt says only the static design check ran and why (GST-1718), from the same
|
||||
// diff-scope the depth reads; fails_when=write records no design field on a frontend range, or records one on a
|
||||
// backend-only range (test above); seam=none
|
||||
test('a template-UI diff records design: static (no preview declared)', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
commit(repo, { 'src/ui/row.ts': "import { html } from 'lit';\nexport const row = (n: string) => html`<div class=\"row\">${n}</div>`;\n" });
|
||||
const body = path.join(repo.root, 'body.md');
|
||||
const ok = writeReceipt(repo, body);
|
||||
expect(ok.status).toBe(EXIT.ok);
|
||||
expect(receiptIn(body).design).toEqual({ mode: 'static', reason: 'no preview declared' });
|
||||
}, 60_000);
|
||||
|
||||
// Value: protects=a receipt names the gstack release that wrote it, never a newer policy key its tree carries, so a
|
||||
// version check against a receipt cannot be passed by an older build (GST-1366); fails_when=write records the
|
||||
// key-inflated version policy reading uses, or policy reading stops seeing the newest key; seam=none
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"bytes": 205,
|
||||
"why": "GST-1718: Design Review line says static (no preview declared); template UI counts as frontend"
|
||||
}
|
||||
Reference in new issue
Block a user