Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/harness/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { handlerNoDirectSdkImport } from "./handler-no-direct-sdk-import.ts";
import { handlerTenantIsolation } from "./handler-tenant-isolation.ts";
import { iamWildcardNeedsJustify } from "./iam-wildcard-needs-justify.ts";
import { lambdaEnvSize } from "./lambda-env-size.ts";
import { noAwsTrademarkFictions } from "./no-aws-trademark-fictions.ts";
import { noConflictMarkers } from "./no-conflict-markers.ts";

export const architectureRules = [
Expand All @@ -20,4 +21,6 @@ export const architectureRules = [
noConflictMarkers,
// Issue #1309 / Lambda env 4KB hard limit 再発防止 (= #1308 root cause)
lambdaEnvSize,
// AWS GameDay branding (= Unicorn.Rentals 等) を OSS / 商用 platform で流用しない // allow-aws-fiction: rule self-description
noAwsTrademarkFictions,
] as const;
91 changes: 91 additions & 0 deletions .claude/harness/src/rules/no-aws-trademark-fictions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { noAwsTrademarkFictions } from "./no-aws-trademark-fictions.ts";

function ctx(files: Record<string, string>) {
return {
files: Object.keys(files),
readFile: (p: string) => files[p] ?? "",
};
}

describe("noAwsTrademarkFictions", () => {
it("should pass when no AWS-branded fictional company names are referenced", () => {
const findings = noAwsTrademarkFictions.check(
ctx({
"README.md": "TenkaCloud is a cloud competition platform.",
"apps/foo/src/bar.ts": "const company = 'Tenryu.Mart';",
}),
);
expect(findings).toEqual([]);
});

it("should flag `Unicorn.Rentals` (= AWS GameDay branding) in README.md", () => {
const findings = noAwsTrademarkFictions.check(
ctx({
"README.md": "Keep Unicorn.Rentals returning 200 while under attack.",
}),
);
expect(findings.length).toBe(1);
expect(findings[0]?.ruleId).toBe("no-aws-trademark-fictions");
expect(findings[0]?.severity).toBe("error");
expect(findings[0]?.line).toBe(1);
expect(findings[0]?.match).toBe("Unicorn.Rentals");
});

it("should flag `Unicorn Rentals` (= space variant) in metadata.json", () => {
const findings = noAwsTrademarkFictions.check(
ctx({
"problems/foo/metadata.json": '{"shortDescription": "the famous Unicorn Rentals scenario"}',
}),
);
expect(findings.length).toBe(1);
expect(findings[0]?.match).toBe("Unicorn Rentals");
});

it("should match case-insensitively (`unicorn.rentals`)", () => {
const findings = noAwsTrademarkFictions.check(
ctx({
"docs/foo.md": "inspired by unicorn.rentals",
}),
);
expect(findings.length).toBe(1);
});

it("should NOT flag a line carrying an `allow-aws-fiction:` exemption marker", () => {
const findings = noAwsTrademarkFictions.check(
ctx({
"docs/lore/world.html":
"<p>Inspired by competitions like Unicorn.Rentals.<!-- allow-aws-fiction: comparison only --></p>",
}),
);
expect(findings).toEqual([]);
});

it("should not scan its own source / test file (= fixtures contain banned strings)", () => {
const findings = noAwsTrademarkFictions.check(
ctx({
".claude/harness/src/rules/no-aws-trademark-fictions.ts": "Unicorn.Rentals",
".claude/harness/src/rules/no-aws-trademark-fictions.test.ts": "Unicorn.Rentals",
}),
);
expect(findings).toEqual([]);
});

it("should ignore non-text extensions", () => {
const findings = noAwsTrademarkFictions.check(
ctx({
"assets/image.png": "Unicorn.Rentals (binary content)",
}),
);
expect(findings).toEqual([]);
});

it("should scan `.html` files (e.g. `docs/lore/world.html`)", () => {
const findings = noAwsTrademarkFictions.check(
ctx({
"docs/lore/world.html": "<p>references Unicorn.Rentals scenario</p>",
}),
);
expect(findings.length).toBe(1);
});
});
100 changes: 100 additions & 0 deletions .claude/harness/src/rules/no-aws-trademark-fictions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Finding, Rule, RuleContext } from "../types.ts";

/**
* AWS GameDay 等の AWS official 競技プログラムが使う 「fictional company」 名は、
* その branding が AWS の登録範囲 (および AWS Training の素材として) なので、
* OSS / 商用配布する競技 platform 側で流用するのは IP リスク。
*
* 実害: もし TenkaCloud の問題本文や README に "Unicorn.Rentals" 等が残っていると、
* AWS GameDay の derivative work と見なされかねない。 本 rule は新規 commit に
* これらの名前が混入するのを防ぐ。
*
* 着想を得るのは自由 (= world-building の inspiration として AWS GameDay を参照するのは
* 問題ない) だが、 具体的な fictional company 名 / character 名は独自に名乗ること。
*
* `docs/lore/world.html` のように 「AWS GameDay からの inspiration」 を明示する文脈で
* 名前を comparison 目的で引きたい場合は、 行末 / 行内に `// allow-aws-fiction:` か
* `<!-- allow-aws-fiction: ... -->` (HTML) のマーカーを置けば例外扱いになる。
*/

const FORBIDDEN_FICTION_NAMES: readonly RegExp[] = [
// AWS GameDay: fictional unicorn-themed online retailer
/\bUnicorn\s*\.?\s*Rentals?\b/i,
];

// AWS official 競技で使われる他の fictional name (将来追加できる枠)
// 例: /\bCookie\.\s*Plus\b/i (= AWS GameDay 別シナリオ) も同様に banned 候補

const SCAN_EXTENSIONS = new Set([
".ts",
".tsx",
".js",
".jsx",
".json",
".md",
".mdx",
".html",
".css",
".yaml",
".yml",
".sh",
".py",
]);

const ALLOW_MARKER = /allow-aws-fiction\b/;

function shouldScan(path: string): boolean {
// 本 rule 自身 / そのテストは検査対象外 (rule 説明やテスト fixture が banned 文字列を含むため)
if (path.endsWith("no-aws-trademark-fictions.ts")) return false;
if (path.endsWith("no-aws-trademark-fictions.test.ts")) return false;
// submodule 配下 (= problems/) は別 repo (TenkaCloudChallenge) 側でも独立に harness する想定だが、
// main 側からも cross-check できるよう scan 対象に含める。
const dotIdx = path.lastIndexOf(".");
if (dotIdx < 0) return false;
return SCAN_EXTENSIONS.has(path.slice(dotIdx));
}

function findFirstForbidden(
content: string,
): { line: number; match: string; pattern: string } | undefined {
const lines = content.split(/\r?\n/);
for (let i = 0; i < lines.length; i += 1) {
const l = lines[i] ?? "";
if (ALLOW_MARKER.test(l)) continue;
for (const re of FORBIDDEN_FICTION_NAMES) {
const m = re.exec(l);
if (m) return { line: i + 1, match: m[0], pattern: re.source };
}
}
return undefined;
}

export const noAwsTrademarkFictions: Rule = {
id: "no-aws-trademark-fictions",
severity: "error",
check(ctx: RuleContext): readonly Finding[] {
const findings: Finding[] = [];
for (const path of ctx.files) {
if (!shouldScan(path)) continue;
let content: string;
try {
content = ctx.readFile(path);
} catch {
continue;
}
const hit = findFirstForbidden(content);
if (!hit) continue;
findings.push({
ruleId: "no-aws-trademark-fictions",
severity: "error",
filePath: path,
line: hit.line,
match: hit.match,
message: `AWS GameDay の fictional company 名 「${hit.match}」 が含まれています。 これは AWS 公式 training の branding で、 OSS / 商用配布する TenkaCloud で流用すると IP リスクになります。`,
recommendation:
"独自の fictional company 名 (例: Tenryu.Mart / Ryuou.Trade 等) に置き換えてください。 inspiration として AWS GameDay を 比較目的で引きたい場合のみ、 該当行末に `<!-- allow-aws-fiction: ... -->` (HTML) または `// allow-aws-fiction:` (code) を置けば exempt にできます。",
});
}
return findings;
},
};
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Five problems ship today (see the catalog repo's [CATALOG.md](https://github.com
| `hello-world` | Challenge | Intro | Read a greeting out of SSM Parameter Store and submit it as a flag. The hello-world for the platform itself. |
| `hello-world-battle` | Battle | Intro | Keep an EC2-hosted nginx frontend + Python API both returning 200 every minute to score uptime. |
| `microservice-migration-battle` | Battle | Advanced | Split a 3-service EC2 monolith into Lambda + ECS Fargate + App Runner under a phased polling clock. |
| `security-battle-royale` | Battle | Advanced | Keep a fictional e-commerce site (`Unicorn.Rentals`) returning 200 while under live attack. Availability over polish. |
| `security-battle-royale` | Battle | Advanced | Keep a fictional e-commerce site (`Tenryu.Mart`) returning 200 while under live attack. Availability over polish. |
| `stackstack` | Battle | Advanced | Production-harden AI-generated scaffolds across 5 axes (auth / network / rate / audit / ux). Managed-runtime cutover multiplies the score 10x. |

> [image needed: catalog screenshot]
Expand Down
15 changes: 11 additions & 4 deletions docs/lore/world.html
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,19 @@ <h2>問題作成者向け 物語チェックリスト</h2>
penalty は問題の難易度に合わせて (= 簡単な hint は -5pt、 致命的な hint は -30pt 等)。</li>
</ol>

<h2>参考: AWS GameDay との関係</h2>
<h2>参考: AWS の競技 program との関係</h2>

<p>
本世界観は AWS GameDay の 「Unicorn Rentals (Unicorn.Rentals)」 や Cookie.Plus などの
narrative-driven competition から着想を得ている。 ただし TenkaCloud は OSS で問題を
追加 / 修正できるため、 lore も community contribution として育てていける形にしている。
本世界観は AWS 公式の narrative-driven cloud competition program (年次の "GameDay" 系
イベント) から着想を得ている。 ただし、 AWS 公式が用いる fictional company 名 /
character 名 (= 当該 program 固有の branding) は流用せず、 TenkaCloud 独自の lore
(天下一武道会 / 天龍 系) を用意している。 OSS / 商用配布での IP 線引きとして、
これを harness rule (<code>no-aws-trademark-fictions</code>) で機械的に enforce する。
</p>

<p>
TenkaCloud は OSS で問題を追加 / 修正できるため、 lore も community contribution と
して育てていける形にしている。
</p>

<p>
Expand Down
Loading