ai-saas-guard 0.43.1 → 0.43.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/commands/scan.js +1 -1
- package/dist/context.d.ts +2 -1
- package/dist/context.js +4 -2
- package/dist/hosted/worker.d.ts +2 -0
- package/dist/hosted/worker.js +9 -9
- package/dist/index.d.ts +2 -1
- package/dist/report/summary.js +43 -0
- package/dist/scanners/gitDiff.js +20 -2
- package/dist/scanners/silentSuccess.js +49 -3
- package/dist/stackInventory.d.ts +5 -0
- package/dist/stackInventory.js +11 -5
- package/dist/types.d.ts +1 -0
- package/dist/utils/files.d.ts +15 -0
- package/dist/utils/files.js +60 -9
- package/docs/CODEX_AGENT_WORKING_RULES.md +58 -0
- package/docs/CODEX_HANDOFF.md +28 -22
- package/docs/CODEX_RECENT_CHANGES.md +139 -1
- package/docs/CODEX_STATE.md +21 -11
- package/docs/CODEX_TODO.md +6 -3
- package/docs/README.zh-CN.md +29 -6
- package/docs/beta-evidence-execution-2026-05-27.md +203 -0
- package/docs/beta-readiness-review-2026-05-27.md +153 -0
- package/docs/cross-project-traffic-evidence-2026-05-27.md +115 -0
- package/docs/design-partner-outreach-kit.md +22 -0
- package/docs/hosted-operational-release-gate.md +2 -2
- package/docs/hosted-operations-evidence.md +35 -1
- package/docs/hosted-operator-runbook.md +1 -1
- package/docs/metrics-snapshot.md +55 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/project-handoff.md +11 -8
- package/docs/public-beta-evidence-feedback.md +23 -6
- package/hosted/cloudflare-worker/README.md +2 -2
- package/hosted/cloudflare-worker/src/index.js +22 -4
- package/package.json +6 -2
- package/scripts/cross-project-discovery-check.mjs +366 -0
- package/scripts/hosted-pr-smoke.mjs +62 -11
- package/scripts/metrics-snapshot.mjs +467 -0
package/README.md
CHANGED
|
@@ -174,6 +174,19 @@ For a concise comparison with Semgrep, zizmor, OpenSSF Scorecard, Snyk, and GitH
|
|
|
174
174
|
|
|
175
175
|
Choose the path by trust boundary: use the **Local CLI** when code must stay on your machine, the **GitHub Action** when you want repeatable CI evidence, and the **Hosted GitHub App** when reviewers need an automatic Check Run that groups auth, billing, tenant-data, deploy, and test-risk areas before merge.
|
|
176
176
|
|
|
177
|
+
## Pre-Commercial Feedback
|
|
178
|
+
|
|
179
|
+
`ai-saas-guard` is looking for privacy-safe design-partner feedback before any public hosted beta decision. The safest path is local:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npx --yes ai-saas-guard@latest demo --summary
|
|
183
|
+
npx --yes ai-saas-guard@latest scan --root /path/to/your-low-risk-demo-repo --summary
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Public-safe feedback belongs in [issue #93](https://github.com/zr9959/ai-saas-guard/issues/93): package version, path used, stack category, severity counts, rule IDs, what felt confusing/noisy/missing, and whether the report would change a launch or merge decision. Do not share source, raw diffs, PR text, logs, secrets, customer data, private URLs, checkout paths, or credentials.
|
|
187
|
+
|
|
188
|
+
Downloads, stars, page views, anonymous comments, simulated scans, and internal assumptions do not count as design-partner evidence.
|
|
189
|
+
|
|
177
190
|
## Quick Start
|
|
178
191
|
|
|
179
192
|
Run the published CLI without installing it globally:
|
|
@@ -235,20 +248,32 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
235
248
|
| Area | Status |
|
|
236
249
|
| --- | --- |
|
|
237
250
|
| Public GitHub repository | Available |
|
|
238
|
-
| npm CLI | `ai-saas-guard@0.43.
|
|
239
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.43.
|
|
251
|
+
| npm CLI | `ai-saas-guard@0.43.3` |
|
|
252
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.43.3` |
|
|
240
253
|
| Outputs | Launch decision queue, short summary, terminal, JSON, SARIF, and PR-focused markdown |
|
|
241
254
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
|
|
242
255
|
| Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
|
|
243
|
-
| Versioned Action tags | `v0.43.
|
|
244
|
-
| Current release | `0.43.
|
|
256
|
+
| Versioned Action tags | `v0.43.3`, `v0` |
|
|
257
|
+
| Current release | `0.43.3` reduces silent-success false positives for Cloudflare Durable Object stubs, benign null-return parsing/cache reads, configuration fallback parameters, and assertion-rich tests while keeping billing disabled |
|
|
245
258
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
246
259
|
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
247
260
|
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; public install/privacy notes are in [docs/hosted-install-privacy.md](docs/hosted-install-privacy.md); signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
|
|
248
261
|
| Hosted GitHub App staging | Private App `ai-saas-guard-hosted` (`3834787`) installed on `zr9959/ai-saas-guard`; hosted operations evidence is in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md) |
|
|
262
|
+
| Public beta readiness | Blocked on real design-partner feedback, deployed source-checkout proof, full GitHub App deletion proof, and provider monitoring evidence for the source-checkout path |
|
|
249
263
|
| OpenSSF Best Practices | Passing badge, project `12955`; `.bestpractices.json` remains the conservative evidence record |
|
|
250
264
|
| Previous roadmap | v0.36.0 plan is tracked in [docs/v0.36-roadmap.md](docs/v0.36-roadmap.md) |
|
|
251
265
|
|
|
266
|
+
## Related TIYBAI Tools
|
|
267
|
+
|
|
268
|
+
Built by [TIYBAI](https://www.tiybai.com/), `ai-saas-guard` stays focused on launch-risk review for AI-built SaaS apps. These adjacent TIYBAI pages are useful for developer workflows without turning this project into a broad cross-promotion surface:
|
|
269
|
+
|
|
270
|
+
- [TIYBAI Toolbox](https://www.tiybai.com/en/tools) for browser-based utilities that do not require installing a separate desktop app.
|
|
271
|
+
- [JSON Formatter](https://www.tiybai.com/en/tools/developer/json-formatter) for checking structured API responses and config snippets.
|
|
272
|
+
- [JWT Decoder](https://www.tiybai.com/en/tools/developer/jwt-decoder) for inspecting token claims during auth review.
|
|
273
|
+
- [URL Encoder / Decoder](https://www.tiybai.com/en/tools/developer/url-encoder) for checking callback URLs, webhook URLs, and encoded query strings.
|
|
274
|
+
- [AI Metadata Generator](https://www.tiybai.com/en/tools/ai/metadata-generator) for preparing public page metadata before launch.
|
|
275
|
+
- [PageStow](https://plugin.tiybai.com/) for saving browser context, sessions, notes, and tasks locally in Chrome.
|
|
276
|
+
|
|
252
277
|
## Example Finding
|
|
253
278
|
|
|
254
279
|
Terminal output is designed to be useful to a reviewer, not just a scanner dashboard.
|
|
@@ -425,7 +450,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
425
450
|
|
|
426
451
|
## GitHub Action
|
|
427
452
|
|
|
428
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.43.
|
|
453
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.43.3` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
429
454
|
|
|
430
455
|
```yaml
|
|
431
456
|
name: ai-saas-guard
|
package/dist/commands/scan.js
CHANGED
|
@@ -33,7 +33,7 @@ export async function scanRepository(options) {
|
|
|
33
33
|
...deployFindings,
|
|
34
34
|
...silentSuccessFindings,
|
|
35
35
|
...actionsReport.findings
|
|
36
|
-
]), { stackInventory });
|
|
36
|
+
]), { stackInventory, fileCollection: context.fileCollection });
|
|
37
37
|
}
|
|
38
38
|
function createSkippedSupabaseReport(rootDir) {
|
|
39
39
|
return createReport("check-supabase", rootDir, [], {
|
package/dist/context.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { type TextFile } from "./utils/files.js";
|
|
1
|
+
import { type FileCollectionDiagnostics, type TextFile } from "./utils/files.js";
|
|
2
2
|
export interface ScanContext {
|
|
3
3
|
rootDir: string;
|
|
4
4
|
files: readonly TextFile[];
|
|
5
5
|
filesByPath: ReadonlyMap<string, TextFile>;
|
|
6
|
+
fileCollection: FileCollectionDiagnostics;
|
|
6
7
|
getFiles: (predicate?: (file: TextFile) => boolean) => TextFile[];
|
|
7
8
|
}
|
|
8
9
|
export type ScanInput = string | ScanContext;
|
package/dist/context.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectTextFilesWithDiagnostics } from "./utils/files.js";
|
|
2
2
|
export async function createScanContext(rootDir) {
|
|
3
|
-
const
|
|
3
|
+
const collection = await collectTextFilesWithDiagnostics(rootDir);
|
|
4
|
+
const { files } = collection;
|
|
4
5
|
const filesByPath = new Map(files.map((file) => [file.path, file]));
|
|
5
6
|
return {
|
|
6
7
|
rootDir,
|
|
7
8
|
files,
|
|
8
9
|
filesByPath,
|
|
10
|
+
fileCollection: collection.diagnostics,
|
|
9
11
|
getFiles(predicate) {
|
|
10
12
|
return predicate ? files.filter(predicate) : [...files];
|
|
11
13
|
}
|
package/dist/hosted/worker.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface HostedReadOnlyCheckoutCommandResult {
|
|
|
15
15
|
stdout: string;
|
|
16
16
|
}
|
|
17
17
|
export type HostedReadOnlyCheckoutCommandRunner = (command: HostedReadOnlyCheckoutCommand) => Promise<HostedReadOnlyCheckoutCommandResult> | HostedReadOnlyCheckoutCommandResult;
|
|
18
|
+
export type HostedCheckoutCleanup = (checkoutDir: string) => Promise<void> | void;
|
|
18
19
|
export type HostedInstallationTokenProvider = (input: HostedServiceScanRunnerInput) => Promise<string> | string;
|
|
19
20
|
export interface HostedReadOnlyCheckoutScanRunnerOptions {
|
|
20
21
|
checkoutRoot?: string;
|
|
@@ -26,6 +27,7 @@ export interface HostedReadOnlyCheckoutScanRunnerOptions {
|
|
|
26
27
|
maxOutputBytes?: number;
|
|
27
28
|
installationTokenProvider: HostedInstallationTokenProvider;
|
|
28
29
|
commandRunner?: HostedReadOnlyCheckoutCommandRunner;
|
|
30
|
+
cleanupCheckout?: HostedCheckoutCleanup;
|
|
29
31
|
}
|
|
30
32
|
export interface HostedSourceCheckoutTrialPlanInput {
|
|
31
33
|
requestedAt: string;
|
package/dist/hosted/worker.js
CHANGED
|
@@ -258,13 +258,13 @@ export async function runHostedReadOnlyCheckoutScan(input, options) {
|
|
|
258
258
|
const maxOutputBytes = clampPositiveInteger(options.maxOutputBytes, HOSTED_WORKER_MAX_OUTPUT_BYTES, HOSTED_WORKER_MAX_OUTPUT_BYTES);
|
|
259
259
|
const fetchDepth = clampPositiveInteger(options.fetchDepth, DEFAULT_FETCH_DEPTH, MAX_FETCH_DEPTH);
|
|
260
260
|
const checkoutRoot = options.checkoutRoot ?? join(tmpdir(), "ai-saas-guard-hosted-checkouts");
|
|
261
|
+
const cleanupCheckout = options.cleanupCheckout ?? defaultCleanupCheckout;
|
|
261
262
|
const token = await options.installationTokenProvider(input);
|
|
262
263
|
if (typeof token !== "string" || token.trim().length === 0) {
|
|
263
264
|
throw new HostedReadOnlyCheckoutScanError("missing_installation_token");
|
|
264
265
|
}
|
|
265
266
|
await mkdir(checkoutRoot, { recursive: true, mode: 0o700 });
|
|
266
267
|
const checkoutDir = await mkdtemp(join(checkoutRoot, "job-"));
|
|
267
|
-
let terminalError;
|
|
268
268
|
try {
|
|
269
269
|
await chmod(checkoutDir, 0o700);
|
|
270
270
|
const askpassPath = join(checkoutDir, ".git-askpass.sh");
|
|
@@ -297,23 +297,23 @@ export async function runHostedReadOnlyCheckoutScan(input, options) {
|
|
|
297
297
|
return compactScanRunnerResult(cliResult.stdout);
|
|
298
298
|
}
|
|
299
299
|
catch (error) {
|
|
300
|
-
terminalError =
|
|
301
|
-
error
|
|
302
|
-
|
|
303
|
-
: new HostedReadOnlyCheckoutScanError("cli_scan_failed");
|
|
300
|
+
const terminalError = error instanceof HostedReadOnlyCheckoutScanError
|
|
301
|
+
? error
|
|
302
|
+
: new HostedReadOnlyCheckoutScanError("cli_scan_failed");
|
|
304
303
|
throw terminalError;
|
|
305
304
|
}
|
|
306
305
|
finally {
|
|
307
306
|
try {
|
|
308
|
-
await
|
|
307
|
+
await cleanupCheckout(checkoutDir);
|
|
309
308
|
}
|
|
310
309
|
catch {
|
|
311
|
-
|
|
312
|
-
throw new HostedReadOnlyCheckoutScanError("cleanup_failed");
|
|
313
|
-
}
|
|
310
|
+
throw new HostedReadOnlyCheckoutScanError("cleanup_failed");
|
|
314
311
|
}
|
|
315
312
|
}
|
|
316
313
|
}
|
|
314
|
+
async function defaultCleanupCheckout(checkoutDir) {
|
|
315
|
+
await rm(checkoutDir, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
317
|
function commandSpec(stage, command, args, cwd, env, timeoutMs, maxOutputBytes) {
|
|
318
318
|
return {
|
|
319
319
|
stage,
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ export { formatSummaryReport } from "./report/summary.js";
|
|
|
13
13
|
export { createLocalScanResourceBudget } from "./performance.js";
|
|
14
14
|
export type { BaseReport, CommandName, Evidence, Finding, ActionsReport, McpOptions, McpPolicyTemplate, McpReport, McpServerInventory, McpSideEffect, PrRiskFile, PrRiskReport, ScanOptions, ShowcaseReport, StripeReport, SupabaseOptions, SupabaseDoctorReport, SupabaseReport } from "./types.js";
|
|
15
15
|
export type { ScanContext, ScanInput } from "./context.js";
|
|
16
|
-
export type { StackCategory, StackEvidence, StackInventory, StackInventoryInput } from "./stackInventory.js";
|
|
16
|
+
export type { StackCategory, StackEvidence, StackInventory, StackInventoryInput, StackInventoryWarning } from "./stackInventory.js";
|
|
17
|
+
export type { FileCollectionDiagnostics, TextFile, TextFileCollection } from "./utils/files.js";
|
|
17
18
|
export type { FindingSuppression, GuardConfig, RuleConfigValue } from "./config.js";
|
|
18
19
|
export type { RuleMetadata, RuleStability } from "./rules/catalog.js";
|
|
19
20
|
export type { LocalScanResourceBudget, LocalScanResourceBudgetInput } from "./performance.js";
|
package/dist/report/summary.js
CHANGED
|
@@ -8,6 +8,9 @@ export function formatSummaryReport(report) {
|
|
|
8
8
|
lines.push(`Findings: ${summaryText(report)}`);
|
|
9
9
|
lines.push(`Launch gate: ${launchGateVerdict(report)}`);
|
|
10
10
|
lines.push(`Decision queue: ${launchDecisionQuestions(report.findings)[0]}`);
|
|
11
|
+
const scanCoverage = scanCoverageText(report);
|
|
12
|
+
if (scanCoverage)
|
|
13
|
+
lines.push(`Scan coverage: ${scanCoverage}`);
|
|
11
14
|
if (report.findings.length > 0) {
|
|
12
15
|
lines.push("Review trust-boundary findings before deploy/cost hygiene.");
|
|
13
16
|
}
|
|
@@ -73,3 +76,43 @@ function summaryText(report) {
|
|
|
73
76
|
return "0 findings";
|
|
74
77
|
return `${report.summary.total} findings: ${report.summary.critical} critical, ${report.summary.high} high, ${report.summary.medium} medium, ${report.summary.low} low, ${report.summary.info} info`;
|
|
75
78
|
}
|
|
79
|
+
function scanCoverageText(report) {
|
|
80
|
+
const parts = [];
|
|
81
|
+
const collection = report.fileCollection;
|
|
82
|
+
if (collection) {
|
|
83
|
+
const unreadableFileCount = collection.unreadableFiles.length;
|
|
84
|
+
const unreadableDirectoryCount = collection.unreadableDirectories.length;
|
|
85
|
+
const skippedLargeCount = collection.skippedLargeFiles.length;
|
|
86
|
+
const skippedBudgetCount = collection.skippedBudgetFiles.length;
|
|
87
|
+
const hasCollectionWarning = unreadableFileCount > 0 ||
|
|
88
|
+
unreadableDirectoryCount > 0 ||
|
|
89
|
+
skippedLargeCount > 0 ||
|
|
90
|
+
skippedBudgetCount > 0 ||
|
|
91
|
+
collection.maxFilesReached ||
|
|
92
|
+
collection.maxTotalBytesReached;
|
|
93
|
+
if (hasCollectionWarning) {
|
|
94
|
+
parts.push(`${collection.filesScanned} ${plural(collection.filesScanned, "file")} scanned`);
|
|
95
|
+
if (unreadableFileCount > 0)
|
|
96
|
+
parts.push(`${unreadableFileCount} unreadable ${plural(unreadableFileCount, "file")}`);
|
|
97
|
+
if (unreadableDirectoryCount > 0) {
|
|
98
|
+
parts.push(`${unreadableDirectoryCount} unreadable ${plural(unreadableDirectoryCount, "directory", "directories")}`);
|
|
99
|
+
}
|
|
100
|
+
if (skippedLargeCount > 0)
|
|
101
|
+
parts.push(`${skippedLargeCount} large ${plural(skippedLargeCount, "file")} skipped`);
|
|
102
|
+
if (skippedBudgetCount > 0)
|
|
103
|
+
parts.push(`${skippedBudgetCount} budget-skipped ${plural(skippedBudgetCount, "file")}`);
|
|
104
|
+
if (collection.maxFilesReached)
|
|
105
|
+
parts.push("file count budget reached");
|
|
106
|
+
if (collection.maxTotalBytesReached)
|
|
107
|
+
parts.push("total byte budget reached");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const malformedPackageCount = report.stackInventory?.warnings.filter((warning) => warning.reason === "invalid_package_json").length ?? 0;
|
|
111
|
+
if (malformedPackageCount > 0) {
|
|
112
|
+
parts.push(`${malformedPackageCount} malformed package ${plural(malformedPackageCount, "manifest")}`);
|
|
113
|
+
}
|
|
114
|
+
return parts.length > 0 ? parts.join("; ") : undefined;
|
|
115
|
+
}
|
|
116
|
+
function plural(count, singular, pluralValue = `${singular}s`) {
|
|
117
|
+
return count === 1 ? singular : pluralValue;
|
|
118
|
+
}
|
package/dist/scanners/gitDiff.js
CHANGED
|
@@ -84,14 +84,28 @@ export async function classifyPrRisk(options) {
|
|
|
84
84
|
}
|
|
85
85
|
async function readGitDiff(rootDir, base) {
|
|
86
86
|
if (base) {
|
|
87
|
+
const tripleDotArgs = ["diff", `${base}...HEAD`];
|
|
87
88
|
try {
|
|
88
|
-
const { stdout } = await execFileAsync("git",
|
|
89
|
+
const { stdout } = await execFileAsync("git", tripleDotArgs, { cwd: rootDir, maxBuffer: 20 * 1024 * 1024 });
|
|
89
90
|
return { diffText: stdout, diagnostics: [] };
|
|
90
91
|
}
|
|
91
92
|
catch (error) {
|
|
93
|
+
if (isMergeBaseUnavailableError(error)) {
|
|
94
|
+
const directDiffArgs = ["diff", `${base}..HEAD`];
|
|
95
|
+
try {
|
|
96
|
+
const { stdout } = await execFileAsync("git", directDiffArgs, { cwd: rootDir, maxBuffer: 20 * 1024 * 1024 });
|
|
97
|
+
return { diffText: stdout, diagnostics: [] };
|
|
98
|
+
}
|
|
99
|
+
catch (fallbackError) {
|
|
100
|
+
return {
|
|
101
|
+
diffText: "",
|
|
102
|
+
diagnostics: [buildGitDiffFailureFinding(rootDir, ["git", ...directDiffArgs], fallbackError, base)]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
92
106
|
return {
|
|
93
107
|
diffText: "",
|
|
94
|
-
diagnostics: [buildGitDiffFailureFinding(rootDir, ["git",
|
|
108
|
+
diagnostics: [buildGitDiffFailureFinding(rootDir, ["git", ...tripleDotArgs], error, base)]
|
|
95
109
|
};
|
|
96
110
|
}
|
|
97
111
|
}
|
|
@@ -146,6 +160,10 @@ function buildGitDiffFailureFinding(rootDir, command, error, base) {
|
|
|
146
160
|
suggestedFix
|
|
147
161
|
});
|
|
148
162
|
}
|
|
163
|
+
function isMergeBaseUnavailableError(error) {
|
|
164
|
+
const lowerError = getGitErrorText(error).toLowerCase();
|
|
165
|
+
return lowerError.includes("no merge base") || lowerError.includes("shallow");
|
|
166
|
+
}
|
|
149
167
|
function buildFetchCommand(base) {
|
|
150
168
|
const remoteRef = /^([^/\s]+)\/(.+)$/.exec(base);
|
|
151
169
|
if (remoteRef)
|
|
@@ -10,6 +10,7 @@ const mockDataPattern = /\b(mock|fixture|fixtures|demo|sample|stub|fake)\b/i;
|
|
|
10
10
|
const bypassPattern = /\b(TODO\s*:?\s*(auth|verify|validation|rate|owner|webhook)|temporary\s+bypass|temp\s+bypass|skip\s+(auth|verification|validation|ownership|webhook)|disable\s+(auth|verification|validation)|SKIP_(AUTH|WEBHOOK|VERIFICATION|VALIDATION)|ALLOW_UNVERIFIED)\b/i;
|
|
11
11
|
const assertionPattern = /\b(expect\s*\(|assert\.|assert\s*\(|t\.is\s*\(|t\.true\s*\(|should\.|toEqual\s*\(|toBe\s*\(|toMatch\s*\()/i;
|
|
12
12
|
const truthyOnlyPattern = /\b(expect\s*\([^)]*\)\.(toBeTruthy|toBeDefined|toBeOk)\s*\(\s*\)|assert\.ok\s*\([^)]*\)|t\.truthy\s*\([^)]*\))/i;
|
|
13
|
+
const strongAssertionPattern = /\b(expect\s*\([^)]*\)\.(?:toEqual|toStrictEqual|toBe|toMatch|toHave|toContain|toThrow|rejects|resolves)\b|assert\.(?:equal|strictEqual|deepEqual|deepStrictEqual|notEqual|notStrictEqual|notDeepEqual|notDeepStrictEqual|match|doesNotMatch|rejects|doesNotReject|throws|doesNotThrow|fail)\s*\(|t\.(?:is|deepEqual|regex|throws|throwsAsync|not)\s*\()/i;
|
|
13
14
|
export async function scanSilentSuccess(input) {
|
|
14
15
|
const context = await resolveScanContext(input);
|
|
15
16
|
const findings = [];
|
|
@@ -32,6 +33,8 @@ export async function scanSilentSuccess(input) {
|
|
|
32
33
|
function scanSwallowedErrors(filePath, content) {
|
|
33
34
|
const findings = [];
|
|
34
35
|
for (const block of findCatchBlocks(content)) {
|
|
36
|
+
if (isBenignNullCatch(content, block))
|
|
37
|
+
continue;
|
|
35
38
|
if (fakeSuccessPattern.test(block.text) && !safeFailurePattern.test(block.text)) {
|
|
36
39
|
findings.push(finding({
|
|
37
40
|
ruleId: "silent-success.swallowed-error",
|
|
@@ -88,12 +91,16 @@ function scanProductionMockData(filePath, content) {
|
|
|
88
91
|
}
|
|
89
92
|
function scanHardcodedFallbacks(filePath, content) {
|
|
90
93
|
const findings = [];
|
|
91
|
-
const fallbackPattern = /(fallback|mock|demo|sample|stub|fake)[\s\S]{0,
|
|
94
|
+
const fallbackPattern = /(fallback|mock|demo|sample|stub|fake)[\s\S]{0,220}(Response\.json|NextResponse\.json|success\s*:\s*true|ok\s*:\s*true|status\s*:\s*["']active["']|active\s*:\s*true|subscription\s*:\s*(?:mock|demo|sample|fallback|fake|\{|\[)|entitlement\s*:\s*(?:true|active|\{|\[))/gi;
|
|
92
95
|
for (const match of content.matchAll(fallbackPattern)) {
|
|
93
96
|
const index = match.index ?? 0;
|
|
94
97
|
const window = content.slice(Math.max(0, index - 180), index + match[0].length + 180);
|
|
95
98
|
if (/degraded\s*:\s*true|status\s*:\s*(4|5)\d\d|error\s*:/i.test(window))
|
|
96
99
|
continue;
|
|
100
|
+
if (isCloudflareDurableObjectStubWindow(window))
|
|
101
|
+
continue;
|
|
102
|
+
if (isConfigurationFallbackWindow(window))
|
|
103
|
+
continue;
|
|
97
104
|
const line = lineNumberForIndex(content, index);
|
|
98
105
|
findings.push(finding({
|
|
99
106
|
ruleId: "silent-success.hardcoded-fallback",
|
|
@@ -134,7 +141,7 @@ function scanTestIntegrity(filePath, content) {
|
|
|
134
141
|
if (body.length === 0 ||
|
|
135
142
|
/^\/\/\s*TODO\b/i.test(body) ||
|
|
136
143
|
!assertionPattern.test(body) ||
|
|
137
|
-
(
|
|
144
|
+
hasOnlyWeakTruthyAssertions(body)) {
|
|
138
145
|
findings.push(testFinding(filePath, content, testCase.line, "Weak or placeholder test may create fake confidence"));
|
|
139
146
|
}
|
|
140
147
|
}
|
|
@@ -167,11 +174,50 @@ function findCatchBlocks(content) {
|
|
|
167
174
|
continue;
|
|
168
175
|
blocks.push({
|
|
169
176
|
text: content.slice(start, end + 1),
|
|
170
|
-
line: lineNumberForIndex(content, match.index ?? 0)
|
|
177
|
+
line: lineNumberForIndex(content, match.index ?? 0),
|
|
178
|
+
start: match.index ?? 0,
|
|
179
|
+
end
|
|
171
180
|
});
|
|
172
181
|
}
|
|
173
182
|
return blocks;
|
|
174
183
|
}
|
|
184
|
+
function isBenignNullCatch(content, block) {
|
|
185
|
+
if (!/\breturn\s+null\s*;?\s*\}/i.test(block.text))
|
|
186
|
+
return false;
|
|
187
|
+
if (/(?:Response|NextResponse)\.json|success\s*:\s*true|ok\s*:\s*true|return\s+true\b|subscription\s*:|entitlement\s*:|active\s*:/i.test(block.text)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const before = content.slice(Math.max(0, block.start - 520), block.start);
|
|
191
|
+
const after = content.slice(block.end + 1, block.end + 720);
|
|
192
|
+
const surrounding = `${before}\n${block.text}\n${after}`;
|
|
193
|
+
if (/(?:atob|btoa|base64|base64url|decodeURIComponent|TextDecoder|JSON\.parse|\bparse[A-Z]|\bdecode[A-Z]|early\s+data)/i.test(surrounding)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (/(?:STATESTORE|DurableObject|Durable Object|idFromName|stub\.fetch|env\.[A-Z0-9_]+\.get\s*\(|KV\.get|env\.KV|get\s*\([^)]*["']json["']|cache|cached|storage|state)/i.test(surrounding) &&
|
|
197
|
+
/(?:KV\.get|env\.KV|stub\.fetch|fetch\s*\(|get\s*\()/i.test(after)) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
function isCloudflareDurableObjectStubWindow(window) {
|
|
203
|
+
return (/\bidFromName\s*\(/i.test(window) &&
|
|
204
|
+
/\benv\.[A-Z0-9_]+\.get\s*\(/i.test(window) &&
|
|
205
|
+
/\bstub\b/i.test(window));
|
|
206
|
+
}
|
|
207
|
+
function isConfigurationFallbackWindow(window) {
|
|
208
|
+
return (/\bfunction\b[^{;=]*\([^)]*\bfallback\s*=\s*(?:\{\}|\[\]|null|undefined|[0-9]+|["'][^"']*["'])/i.test(window) &&
|
|
209
|
+
!/(?:Response|NextResponse)\.json|success\s*:\s*true|ok\s*:\s*true|active\s*:\s*true|status\s*:\s*["']active["']|subscription\s*:\s*(?:mock|demo|sample|fallback|fake|\{|\[)|entitlement\s*:\s*(?:true|active|\{|\[)/i.test(window));
|
|
210
|
+
}
|
|
211
|
+
function hasOnlyWeakTruthyAssertions(body) {
|
|
212
|
+
if (!truthyOnlyPattern.test(body))
|
|
213
|
+
return false;
|
|
214
|
+
if (strongAssertionPattern.test(body))
|
|
215
|
+
return false;
|
|
216
|
+
if (/\bassert\.ok\s*\([^)]*(?:[=!]==?|[<>]=?|\.(?:length|size)\b|\.includes\s*\(|\.some\s*\(|\.every\s*\()/i.test(body)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
175
221
|
function findTestBlocks(content) {
|
|
176
222
|
const blocks = [];
|
|
177
223
|
const testPattern = /\b(?:test|it)\s*\(\s*["'`][^"'`]+["'`]\s*,\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/gi;
|
package/dist/stackInventory.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface StackEvidence {
|
|
|
6
6
|
file: string;
|
|
7
7
|
reason: string;
|
|
8
8
|
}
|
|
9
|
+
export interface StackInventoryWarning {
|
|
10
|
+
file: string;
|
|
11
|
+
reason: "invalid_package_json";
|
|
12
|
+
}
|
|
9
13
|
export interface StackInventory {
|
|
10
14
|
frameworks: string[];
|
|
11
15
|
databases: string[];
|
|
@@ -15,6 +19,7 @@ export interface StackInventory {
|
|
|
15
19
|
storage: string[];
|
|
16
20
|
deploy: string[];
|
|
17
21
|
evidence: StackEvidence[];
|
|
22
|
+
warnings: StackInventoryWarning[];
|
|
18
23
|
}
|
|
19
24
|
export type StackInventoryInput = ScanInput | {
|
|
20
25
|
rootDir: string;
|
package/dist/stackInventory.js
CHANGED
|
@@ -22,7 +22,8 @@ function createMutableInventory() {
|
|
|
22
22
|
payments: new Set(),
|
|
23
23
|
storage: new Set(),
|
|
24
24
|
deploy: new Set(),
|
|
25
|
-
evidence: []
|
|
25
|
+
evidence: [],
|
|
26
|
+
warnings: []
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
function addEvidence(inventory, category, tool, file, reason) {
|
|
@@ -95,7 +96,11 @@ function detectFromContent(file, inventory) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
function detectPackageJson(file, inventory) {
|
|
98
|
-
const
|
|
99
|
+
const packageInventory = readPackageDependencies(file.content);
|
|
100
|
+
if (packageInventory.parseError) {
|
|
101
|
+
inventory.warnings.push({ file: file.path, reason: "invalid_package_json" });
|
|
102
|
+
}
|
|
103
|
+
const dependencies = packageInventory.dependencies;
|
|
99
104
|
const has = (name) => dependencies.has(name);
|
|
100
105
|
const hasAny = (names) => names.some((name) => has(name));
|
|
101
106
|
if (has("next"))
|
|
@@ -196,10 +201,10 @@ function readPackageDependencies(content) {
|
|
|
196
201
|
for (const name of Object.keys(values))
|
|
197
202
|
result.add(name);
|
|
198
203
|
}
|
|
199
|
-
return result;
|
|
204
|
+
return { dependencies: result, parseError: false };
|
|
200
205
|
}
|
|
201
206
|
catch {
|
|
202
|
-
return new Set();
|
|
207
|
+
return { dependencies: new Set(), parseError: true };
|
|
203
208
|
}
|
|
204
209
|
}
|
|
205
210
|
function detectPythonStack(file, inventory) {
|
|
@@ -245,6 +250,7 @@ function finalizeInventory(inventory) {
|
|
|
245
250
|
payments: [...inventory.payments].sort(),
|
|
246
251
|
storage: [...inventory.storage].sort(),
|
|
247
252
|
deploy: [...inventory.deploy].sort(),
|
|
248
|
-
evidence: [...inventory.evidence].sort((a, b) => `${a.category}:${a.tool}:${a.file}`.localeCompare(`${b.category}:${b.tool}:${b.file}`))
|
|
253
|
+
evidence: [...inventory.evidence].sort((a, b) => `${a.category}:${a.tool}:${a.file}`.localeCompare(`${b.category}:${b.tool}:${b.file}`)),
|
|
254
|
+
warnings: [...inventory.warnings].sort((a, b) => `${a.reason}:${a.file}`.localeCompare(`${b.reason}:${b.file}`))
|
|
249
255
|
};
|
|
250
256
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface BaseReport {
|
|
|
40
40
|
findings: Finding[];
|
|
41
41
|
summary: Summary;
|
|
42
42
|
stackInventory?: import("./stackInventory.js").StackInventory;
|
|
43
|
+
fileCollection?: import("./utils/files.js").FileCollectionDiagnostics;
|
|
43
44
|
}
|
|
44
45
|
export interface ShowcaseReport extends BaseReport {
|
|
45
46
|
command: "demo";
|
package/dist/utils/files.d.ts
CHANGED
|
@@ -8,10 +8,25 @@ export interface CollectTextFilesOptions {
|
|
|
8
8
|
maxFiles?: number;
|
|
9
9
|
maxTotalBytes?: number;
|
|
10
10
|
}
|
|
11
|
+
export interface FileCollectionDiagnostics {
|
|
12
|
+
filesScanned: number;
|
|
13
|
+
bytesScanned: number;
|
|
14
|
+
unreadableFiles: string[];
|
|
15
|
+
unreadableDirectories: string[];
|
|
16
|
+
skippedLargeFiles: string[];
|
|
17
|
+
skippedBudgetFiles: string[];
|
|
18
|
+
maxFilesReached: boolean;
|
|
19
|
+
maxTotalBytesReached: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface TextFileCollection {
|
|
22
|
+
files: TextFile[];
|
|
23
|
+
diagnostics: FileCollectionDiagnostics;
|
|
24
|
+
}
|
|
11
25
|
export declare const DEFAULT_MAX_TEXT_FILE_BYTES: number;
|
|
12
26
|
export declare const DEFAULT_MAX_TEXT_FILES = 10000;
|
|
13
27
|
export declare const DEFAULT_MAX_TOTAL_TEXT_BYTES: number;
|
|
14
28
|
export declare function collectTextFiles(rootDir: string, options?: CollectTextFilesOptions): Promise<TextFile[]>;
|
|
29
|
+
export declare function collectTextFilesWithDiagnostics(rootDir: string, options?: CollectTextFilesOptions): Promise<TextFileCollection>;
|
|
15
30
|
export declare function lineNumberForIndex(content: string, index: number): number;
|
|
16
31
|
export declare function lineAt(content: string, lineNumber: number): string;
|
|
17
32
|
export declare function toPosix(path: string): string;
|
package/dist/utils/files.js
CHANGED
|
@@ -15,6 +15,9 @@ const ignoredDirectories = new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
const textFilePattern = /(^|\/)(\.env[^/]*|\.mcp\.json|mcp\.json|claude_desktop_config\.json|Dockerfile)$|\.(cjs|cts|js|jsx|json|mjs|mts|prisma|sql|toml|ts|tsx|yaml|yml|env|md|txt)$/i;
|
|
17
17
|
export async function collectTextFiles(rootDir, options = {}) {
|
|
18
|
+
return (await collectTextFilesWithDiagnostics(rootDir, options)).files;
|
|
19
|
+
}
|
|
20
|
+
export async function collectTextFilesWithDiagnostics(rootDir, options = {}) {
|
|
18
21
|
const files = [];
|
|
19
22
|
const ignores = await loadIgnoreRules(rootDir);
|
|
20
23
|
const budget = {
|
|
@@ -23,22 +26,38 @@ export async function collectTextFiles(rootDir, options = {}) {
|
|
|
23
26
|
maxTotalBytes: positiveIntegerOrDefault(options.maxTotalBytes, DEFAULT_MAX_TOTAL_TEXT_BYTES),
|
|
24
27
|
collectedBytes: 0
|
|
25
28
|
};
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
const diagnostics = createFileCollectionDiagnostics();
|
|
30
|
+
await walk(rootDir, rootDir, files, ignores, budget, diagnostics);
|
|
31
|
+
diagnostics.filesScanned = files.length;
|
|
32
|
+
diagnostics.bytesScanned = budget.collectedBytes;
|
|
33
|
+
return { files, diagnostics };
|
|
28
34
|
}
|
|
29
|
-
async function walk(rootDir, currentDir, files, ignores, budget) {
|
|
30
|
-
if (files.length >= budget.maxFiles
|
|
35
|
+
async function walk(rootDir, currentDir, files, ignores, budget, diagnostics) {
|
|
36
|
+
if (files.length >= budget.maxFiles) {
|
|
37
|
+
diagnostics.maxFilesReached = true;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (budget.collectedBytes >= budget.maxTotalBytes) {
|
|
41
|
+
diagnostics.maxTotalBytesReached = true;
|
|
31
42
|
return;
|
|
43
|
+
}
|
|
32
44
|
let entries;
|
|
33
45
|
try {
|
|
34
46
|
entries = await readdir(currentDir, { withFileTypes: true });
|
|
35
47
|
}
|
|
36
48
|
catch {
|
|
49
|
+
pushUnique(diagnostics.unreadableDirectories, relativeCollectionPath(rootDir, currentDir));
|
|
37
50
|
return;
|
|
38
51
|
}
|
|
39
52
|
for (const entry of entries) {
|
|
40
|
-
if (files.length >= budget.maxFiles
|
|
53
|
+
if (files.length >= budget.maxFiles) {
|
|
54
|
+
diagnostics.maxFilesReached = true;
|
|
41
55
|
break;
|
|
56
|
+
}
|
|
57
|
+
if (budget.collectedBytes >= budget.maxTotalBytes) {
|
|
58
|
+
diagnostics.maxTotalBytesReached = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
42
61
|
if (entry.name === ".DS_Store" || entry.name.startsWith("._"))
|
|
43
62
|
continue;
|
|
44
63
|
const absolutePath = join(currentDir, entry.name);
|
|
@@ -48,16 +67,28 @@ async function walk(rootDir, currentDir, files, ignores, budget) {
|
|
|
48
67
|
if (entry.isDirectory()) {
|
|
49
68
|
if (ignoredDirectories.has(entry.name))
|
|
50
69
|
continue;
|
|
51
|
-
await walk(rootDir, absolutePath, files, ignores, budget);
|
|
70
|
+
await walk(rootDir, absolutePath, files, ignores, budget, diagnostics);
|
|
52
71
|
continue;
|
|
53
72
|
}
|
|
54
73
|
if (!entry.isFile() || !textFilePattern.test(relativePath))
|
|
55
74
|
continue;
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
let fileStat;
|
|
76
|
+
try {
|
|
77
|
+
fileStat = await stat(absolutePath);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
pushUnique(diagnostics.unreadableFiles, relativePath);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (fileStat.size > budget.maxFileBytes) {
|
|
84
|
+
pushUnique(diagnostics.skippedLargeFiles, relativePath);
|
|
58
85
|
continue;
|
|
59
|
-
|
|
86
|
+
}
|
|
87
|
+
if (budget.collectedBytes + fileStat.size > budget.maxTotalBytes) {
|
|
88
|
+
diagnostics.maxTotalBytesReached = true;
|
|
89
|
+
pushUnique(diagnostics.skippedBudgetFiles, relativePath);
|
|
60
90
|
continue;
|
|
91
|
+
}
|
|
61
92
|
try {
|
|
62
93
|
files.push({
|
|
63
94
|
path: relativePath,
|
|
@@ -67,10 +98,30 @@ async function walk(rootDir, currentDir, files, ignores, budget) {
|
|
|
67
98
|
budget.collectedBytes += fileStat.size;
|
|
68
99
|
}
|
|
69
100
|
catch {
|
|
101
|
+
pushUnique(diagnostics.unreadableFiles, relativePath);
|
|
70
102
|
continue;
|
|
71
103
|
}
|
|
72
104
|
}
|
|
73
105
|
}
|
|
106
|
+
function createFileCollectionDiagnostics() {
|
|
107
|
+
return {
|
|
108
|
+
filesScanned: 0,
|
|
109
|
+
bytesScanned: 0,
|
|
110
|
+
unreadableFiles: [],
|
|
111
|
+
unreadableDirectories: [],
|
|
112
|
+
skippedLargeFiles: [],
|
|
113
|
+
skippedBudgetFiles: [],
|
|
114
|
+
maxFilesReached: false,
|
|
115
|
+
maxTotalBytesReached: false
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function relativeCollectionPath(rootDir, absolutePath) {
|
|
119
|
+
return toPosix(relative(rootDir, absolutePath)) || ".";
|
|
120
|
+
}
|
|
121
|
+
function pushUnique(target, value) {
|
|
122
|
+
if (!target.includes(value))
|
|
123
|
+
target.push(value);
|
|
124
|
+
}
|
|
74
125
|
export function lineNumberForIndex(content, index) {
|
|
75
126
|
let line = 1;
|
|
76
127
|
for (let position = 0; position < index; position += 1) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Codex Agent Working Rules
|
|
2
|
+
|
|
3
|
+
This project uses these rules for Codex or other coding agents working in `ai-saas-guard`.
|
|
4
|
+
|
|
5
|
+
They adapt general agent-coding discipline to this repository's current boundary: pre-commercial evidence work, local-first scanner behavior, strict secret protection, and cleanup after every task.
|
|
6
|
+
|
|
7
|
+
## 1. Think Before Editing
|
|
8
|
+
|
|
9
|
+
- Read the handoff docs and the relevant project files before changing anything.
|
|
10
|
+
- State assumptions when they affect risk, scope, deployment, secrets, user data, or release status.
|
|
11
|
+
- If a request has multiple plausible meanings, choose the lowest-risk interpretation when it stays inside the user's explicit goal.
|
|
12
|
+
- Stop and ask only when a reasonable assumption would be destructive, secret-bearing, commercial, or likely to erase user-owned work.
|
|
13
|
+
|
|
14
|
+
## 2. Keep Changes Narrow
|
|
15
|
+
|
|
16
|
+
- Every changed line must trace to the user's current request.
|
|
17
|
+
- Do not refactor adjacent code, rewrite docs broadly, reformat unrelated files, or delete pre-existing dead code unless asked.
|
|
18
|
+
- Match local style even when a different style would also work.
|
|
19
|
+
- Prefer documentation/evidence updates over product expansion when the current blocker is missing proof or real feedback.
|
|
20
|
+
|
|
21
|
+
## 3. Prefer Simpler Proof
|
|
22
|
+
|
|
23
|
+
- Solve the smallest real problem that moves the gate forward.
|
|
24
|
+
- Do not add configurability, abstractions, dashboards, analytics, paid packaging, or workflows just because they might be useful later.
|
|
25
|
+
- For scanner behavior, prefer focused rules with synthetic fixtures and clear false-positive boundaries.
|
|
26
|
+
- For hosted readiness, do not replace provider evidence with a boolean, assumption, or local-only test.
|
|
27
|
+
|
|
28
|
+
## 4. Verify Before Claiming
|
|
29
|
+
|
|
30
|
+
- No success claim without fresh evidence from the current turn.
|
|
31
|
+
- For docs-only changes, run at least `git diff --check` and the smallest relevant test set; run `npm test` when public docs or release-gate docs change.
|
|
32
|
+
- For runtime/scanner changes, run `npm test`, focused scanner checks, and any affected CLI commands.
|
|
33
|
+
- If a command fails, report the exact failing area and fix it before committing or merging.
|
|
34
|
+
|
|
35
|
+
## 5. Protect Secrets And Evidence
|
|
36
|
+
|
|
37
|
+
- Never print, commit, rotate, overwrite, or expose secrets, tokens, private keys, cookies, certs, database URLs, customer data, installation tokens, raw webhook payloads, raw diffs, PR text, source, private URLs, checkout paths, or raw provider logs.
|
|
38
|
+
- Do not bulk-delete Cloudflare KV records or remove GitHub App installations unless the user explicitly approves a safe scoped proof.
|
|
39
|
+
- Keep historical rows in `docs/hosted-operations-evidence.md`; append new evidence instead of rewriting history.
|
|
40
|
+
- Keep `.local/` private and ignored.
|
|
41
|
+
|
|
42
|
+
## 6. Use Risk-Gated Autonomy
|
|
43
|
+
|
|
44
|
+
- Continue automatically for safe read-only checks, docs updates, tests, PR creation, and cleanup when the user has asked to proceed.
|
|
45
|
+
- Pause only for destructive operations, secret access, GitHub App installation mutation, Cloudflare deployment, KV deletion, npm publishing, or commercialization work.
|
|
46
|
+
- Do not start billing, pricing, paid packaging, marketplace conversion, sales funnel, broad analytics, or customer account work.
|
|
47
|
+
- If the next valid progress requires real users or design partners, record the blocker without fabricating evidence.
|
|
48
|
+
|
|
49
|
+
## 7. Clean Up Every Time
|
|
50
|
+
|
|
51
|
+
- Remove temporary files generated under `/tmp` or the repo before finishing.
|
|
52
|
+
- Stop or wait for any test, watch, dev-server, deploy, smoke-test, or GitHub-watch process started during the task.
|
|
53
|
+
- Confirm `git status -sb` and mention any intentional remaining branch/PR state.
|
|
54
|
+
- If README.md changes, check and update `docs/README.zh-CN.md` in the same task.
|
|
55
|
+
|
|
56
|
+
## Working Test
|
|
57
|
+
|
|
58
|
+
These rules are working when diffs are small, every claim has verification evidence, public docs stay aligned, no unrelated files are touched, no secrets are exposed, and public beta remains blocked until real design-partner and provider evidence exists.
|