ai-project-maintainer 0.3.1 → 0.4.1
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 +45 -9
- package/ai-project-maintainer/agents/openai.yaml +6 -6
- package/ai-project-maintainer/references/ci-guardrails.md +55 -55
- package/ai-project-maintainer/references/database.md +60 -60
- package/ai-project-maintainer/references/electron-desktop.md +43 -43
- package/ai-project-maintainer/references/incident-response.md +52 -52
- package/ai-project-maintainer/references/security.md +48 -48
- package/ai-project-maintainer/references/tool-router.md +53 -53
- package/ai-project-maintainer/scripts/bootstrap-local-tools.ps1 +109 -109
- package/ai-project-maintainer/scripts/ci-smoke-gate.mjs +26 -26
- package/ai-project-maintainer/scripts/init-project.mjs +28 -16
- package/ai-project-maintainer/scripts/lib/check-registry.mjs +10 -9
- package/ai-project-maintainer/scripts/lib/checks.mjs +22 -10
- package/ai-project-maintainer/scripts/lib/command-runner.mjs +17 -3
- package/ai-project-maintainer/scripts/lib/policy.mjs +6 -4
- package/ai-project-maintainer/scripts/lib/report.mjs +56 -32
- package/assets/demo-90s-storyboard.svg +98 -0
- package/assets/demo-90s.gif +0 -0
- package/assets/social-preview.png +0 -0
- package/assets/social-preview.svg +55 -0
- package/docs/DEMO.md +39 -44
- package/docs/DEMO.zh-CN.md +40 -46
- package/docs/GITHUB-LAUNCH-CHECKLIST.md +11 -11
- package/docs/POLICY-AND-EXCEPTIONS.zh-CN.md +1 -1
- package/docs/PROMOTION.md +49 -21
- package/docs/SECURITY-WORKFLOW.md +63 -0
- package/docs/UPGRADE-ROADMAP.zh-CN.md +28 -27
- package/docs/demo-output/90-second-demo.html +187 -0
- package/docs/demo-output/before-after-case.md +91 -0
- package/docs/demo-output/security-report.md +45 -37
- package/docs/superpowers/plans/2026-06-29-ci-dogfooding.md +200 -200
- package/examples/demo-ai-app/.ai-maintainer/business-flows.yml +14 -0
- package/examples/demo-ai-app/.ai-maintainer/db-migration-policy.yml +6 -0
- package/examples/demo-ai-app/.ai-maintainer/evidence-sources.yml +18 -0
- package/examples/demo-ai-app/.ai-maintainer/exceptions.yml +1 -0
- package/examples/demo-ai-app/.ai-maintainer/incident-runbook.md +11 -0
- package/examples/demo-ai-app/.ai-maintainer/observability-checklist.yml +7 -0
- package/examples/demo-ai-app/.ai-maintainer/policy.yml +27 -0
- package/examples/demo-ai-app/.ai-maintainer/project-profile.yml +15 -0
- package/examples/demo-ai-app/.ai-maintainer/release-checklist.yml +7 -0
- package/examples/demo-ai-app/.ai-maintainer/risk-policy.yml +5 -0
- package/examples/demo-ai-app/.ai-maintainer/threat-model.md +18 -0
- package/examples/demo-ai-app/README.md +38 -0
- package/examples/demo-ai-app/package-lock.json +15 -0
- package/examples/demo-ai-app/package.json +16 -0
- package/examples/demo-ai-app/scripts/build.mjs +18 -0
- package/examples/demo-ai-app/scripts/create-before-state.mjs +86 -0
- package/examples/demo-ai-app/scripts/run-demo-gate.mjs +95 -0
- package/examples/demo-ai-app/src/order-risk.js +28 -0
- package/examples/demo-ai-app/test/order-risk.test.mjs +24 -0
- package/package.json +11 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const ignored = new Set(["node_modules", "dist", "reports"]);
|
|
8
|
+
|
|
9
|
+
function normalizeForCompare(value) {
|
|
10
|
+
return path.resolve(value).toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function assertInsideTemp(destination) {
|
|
14
|
+
const tempRoot = normalizeForCompare(fs.realpathSync(os.tmpdir()));
|
|
15
|
+
const resolved = normalizeForCompare(destination);
|
|
16
|
+
if (resolved !== tempRoot && !resolved.startsWith(`${tempRoot}${path.sep}`)) {
|
|
17
|
+
throw new Error(`Refusing to write demo before-state outside the OS temp directory: ${destination}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function copyDirectory(source, destination) {
|
|
22
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
23
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
24
|
+
if (ignored.has(entry.name)) continue;
|
|
25
|
+
const from = path.join(source, entry.name);
|
|
26
|
+
const to = path.join(destination, entry.name);
|
|
27
|
+
if (entry.isDirectory()) copyDirectory(from, to);
|
|
28
|
+
else if (entry.isFile()) fs.copyFileSync(from, to);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readOutputArg(args) {
|
|
33
|
+
const index = args.indexOf("--output");
|
|
34
|
+
if (index !== -1) return args[index + 1];
|
|
35
|
+
const inline = args.find((arg) => arg.startsWith("--output="));
|
|
36
|
+
return inline ? inline.slice("--output=".length) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createBeforeState({ outputPath = null } = {}) {
|
|
40
|
+
const demoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
41
|
+
const destination = path.resolve(outputPath || path.join(os.tmpdir(), `apm-demo-before-${Date.now()}`));
|
|
42
|
+
assertInsideTemp(destination);
|
|
43
|
+
|
|
44
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
45
|
+
copyDirectory(demoRoot, destination);
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(
|
|
48
|
+
path.join(destination, "src", "order-risk.js"),
|
|
49
|
+
`const shippingRates = {
|
|
50
|
+
standard: 499,
|
|
51
|
+
expedited: 1299,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function quoteOrder({ subtotalCents, shippingTier }) {
|
|
55
|
+
if (!Number.isInteger(subtotalCents) || subtotalCents < 0) {
|
|
56
|
+
throw new TypeError("subtotalCents must be a non-negative integer");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!Object.hasOwn(shippingRates, shippingTier)) {
|
|
60
|
+
throw new RangeError(\`unsupported shipping tier: \${shippingTier}\`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const shippingCents = shippingRates[shippingTier];
|
|
64
|
+
const totalCents = subtotalCents + shippingCents;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
subtotalCents,
|
|
68
|
+
shippingCents,
|
|
69
|
+
totalCents,
|
|
70
|
+
needsManualReview: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function canReleaseOrder({ paid, flagged }) {
|
|
75
|
+
return Boolean(paid && !flagged);
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return { destination };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
84
|
+
const result = createBeforeState({ outputPath: readOutputArg(process.argv.slice(2)) });
|
|
85
|
+
console.log(JSON.stringify(result, null, 2));
|
|
86
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { runLocalGate } from "../../../ai-project-maintainer/scripts/run-local-gate.mjs";
|
|
8
|
+
import { toMarkdown } from "../../../ai-project-maintainer/scripts/lib/report.mjs";
|
|
9
|
+
|
|
10
|
+
function writeMockTool(toolsDir) {
|
|
11
|
+
const scriptPath = path.join(toolsDir, "mock-tool.mjs");
|
|
12
|
+
fs.writeFileSync(
|
|
13
|
+
scriptPath,
|
|
14
|
+
`#!/usr/bin/env node
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
|
|
17
|
+
const [, , tool, ...args] = process.argv;
|
|
18
|
+
if (args.includes("--version")) {
|
|
19
|
+
console.log(\`\${tool} demo 0.0.0\`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (tool === "syft") {
|
|
24
|
+
const outputArg = args.find((arg) => arg.startsWith("cyclonedx-json="));
|
|
25
|
+
if (outputArg) {
|
|
26
|
+
fs.writeFileSync(outputArg.slice("cyclonedx-json=".length), JSON.stringify({
|
|
27
|
+
bomFormat: "CycloneDX",
|
|
28
|
+
specVersion: "1.5",
|
|
29
|
+
version: 1,
|
|
30
|
+
metadata: { component: { type: "application", name: "demo-ai-app" } },
|
|
31
|
+
components: [],
|
|
32
|
+
}, null, 2));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (tool === "scorecard") {
|
|
37
|
+
console.log(JSON.stringify({ score: 8.5, checks: [] }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.exit(0);
|
|
41
|
+
`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const tools = [
|
|
45
|
+
"actionlint",
|
|
46
|
+
"checkov",
|
|
47
|
+
"gitleaks",
|
|
48
|
+
"grype",
|
|
49
|
+
"mega-linter-runner",
|
|
50
|
+
"osv-scanner",
|
|
51
|
+
"pre-commit",
|
|
52
|
+
"scorecard",
|
|
53
|
+
"semgrep",
|
|
54
|
+
"squawk",
|
|
55
|
+
"syft",
|
|
56
|
+
"trivy",
|
|
57
|
+
"zizmor",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const tool of tools) {
|
|
61
|
+
if (process.platform === "win32") {
|
|
62
|
+
fs.writeFileSync(path.join(toolsDir, `${tool}.cmd`), `@echo off\r\nnode "%~dp0mock-tool.mjs" ${tool} %*\r\n`);
|
|
63
|
+
} else {
|
|
64
|
+
const shim = path.join(toolsDir, tool);
|
|
65
|
+
fs.writeFileSync(shim, `#!/usr/bin/env sh\nnode "$(dirname "$0")/mock-tool.mjs" ${tool} "$@"\n`);
|
|
66
|
+
fs.chmodSync(shim, 0o755);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function runDemoGate({ outputPath = null } = {}) {
|
|
72
|
+
const demoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
73
|
+
const reportsDir = path.join(demoRoot, "reports");
|
|
74
|
+
const toolsDir = fs.mkdtempSync(path.join(os.tmpdir(), "apm-demo-tools-"));
|
|
75
|
+
writeMockTool(toolsDir);
|
|
76
|
+
|
|
77
|
+
const report = runLocalGate(demoRoot, {
|
|
78
|
+
strict: true,
|
|
79
|
+
release: true,
|
|
80
|
+
production: true,
|
|
81
|
+
outputPath: outputPath || path.join(reportsDir, "security-report.json"),
|
|
82
|
+
writeReports: true,
|
|
83
|
+
runnerOptions: {
|
|
84
|
+
envPath: `${toolsDir}${path.delimiter}${process.env.PATH || ""}`,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { report, toolsDir };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
92
|
+
const { report } = runDemoGate();
|
|
93
|
+
console.log(toMarkdown(report));
|
|
94
|
+
process.exit(report.passed ? 0 : 1);
|
|
95
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const shippingRates = {
|
|
2
|
+
standard: 499,
|
|
3
|
+
expedited: 1299,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function quoteOrder({ subtotalCents, shippingTier }) {
|
|
7
|
+
if (!Number.isInteger(subtotalCents) || subtotalCents < 0) {
|
|
8
|
+
throw new TypeError("subtotalCents must be a non-negative integer");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!Object.hasOwn(shippingRates, shippingTier)) {
|
|
12
|
+
throw new RangeError(`unsupported shipping tier: ${shippingTier}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const shippingCents = shippingRates[shippingTier];
|
|
16
|
+
const totalCents = subtotalCents + shippingCents;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
subtotalCents,
|
|
20
|
+
shippingCents,
|
|
21
|
+
totalCents,
|
|
22
|
+
needsManualReview: totalCents >= 100000,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function canReleaseOrder({ paid, flagged, stockAvailable }) {
|
|
27
|
+
return Boolean(paid && stockAvailable && !flagged);
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { canReleaseOrder, quoteOrder } from "../src/order-risk.js";
|
|
5
|
+
|
|
6
|
+
test("checkout quote preserves the customer-visible total", () => {
|
|
7
|
+
assert.deepEqual(quoteOrder({ subtotalCents: 2500, shippingTier: "standard" }), {
|
|
8
|
+
subtotalCents: 2500,
|
|
9
|
+
shippingCents: 499,
|
|
10
|
+
totalCents: 2999,
|
|
11
|
+
needsManualReview: false,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("expensive orders require manual review before release", () => {
|
|
16
|
+
assert.equal(quoteOrder({ subtotalCents: 99600, shippingTier: "standard" }).needsManualReview, true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("orders are released only when payment, stock, and risk checks all pass", () => {
|
|
20
|
+
assert.equal(canReleaseOrder({ paid: true, stockAvailable: true, flagged: false }), true);
|
|
21
|
+
assert.equal(canReleaseOrder({ paid: false, stockAvailable: true, flagged: false }), false);
|
|
22
|
+
assert.equal(canReleaseOrder({ paid: true, stockAvailable: false, flagged: false }), false);
|
|
23
|
+
assert.equal(canReleaseOrder({ paid: true, stockAvailable: true, flagged: true }), false);
|
|
24
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-project-maintainer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Production-readiness audit and CI gate for AI-coded projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,9 +29,17 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"ai-project-maintainer/",
|
|
32
|
+
"assets/",
|
|
33
|
+
"examples/demo-ai-app/.ai-maintainer/",
|
|
34
|
+
"examples/demo-ai-app/package.json",
|
|
35
|
+
"examples/demo-ai-app/package-lock.json",
|
|
36
|
+
"examples/demo-ai-app/README.md",
|
|
37
|
+
"examples/demo-ai-app/scripts/",
|
|
38
|
+
"examples/demo-ai-app/src/",
|
|
39
|
+
"examples/demo-ai-app/test/",
|
|
32
40
|
"docs/",
|
|
33
|
-
"README.md"
|
|
34
|
-
],
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
35
43
|
"bin": {
|
|
36
44
|
"ai-project-maintainer": "ai-project-maintainer/scripts/cli.mjs"
|
|
37
45
|
},
|