id3-cli 0.9.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.ja-JP.md +769 -0
- package/README.ko-KR.md +769 -0
- package/README.md +769 -0
- package/README.tr-TR.md +769 -0
- package/README.zh-CN.md +769 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +40 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/scripts/build-hooks.d.ts +1 -0
- package/dist/scripts/build-hooks.js +58 -0
- package/dist/scripts/build-hooks.js.map +1 -0
- package/dist/src/hooks/auto-audit.d.ts +4 -0
- package/dist/src/hooks/auto-audit.js +47 -0
- package/dist/src/hooks/auto-audit.js.map +1 -0
- package/dist/src/hooks/claude-pretool-entry.d.ts +1 -0
- package/dist/src/hooks/claude-pretool-entry.js +36 -0
- package/dist/src/hooks/claude-pretool-entry.js.map +1 -0
- package/dist/src/hooks/claude-stop-entry.d.ts +1 -0
- package/dist/src/hooks/claude-stop-entry.js +7 -0
- package/dist/src/hooks/claude-stop-entry.js.map +1 -0
- package/dist/src/hooks/post-commit-entry.d.ts +1 -0
- package/dist/src/hooks/post-commit-entry.js +7 -0
- package/dist/src/hooks/post-commit-entry.js.map +1 -0
- package/dist/src/hooks/pre-commit-entry.d.ts +1 -0
- package/dist/src/hooks/pre-commit-entry.js +16 -0
- package/dist/src/hooks/pre-commit-entry.js.map +1 -0
- package/dist/src/hooks/rule-check.d.ts +8 -0
- package/dist/src/hooks/rule-check.js +101 -0
- package/dist/src/hooks/rule-check.js.map +1 -0
- package/dist/src/hooks/schema-drift.d.ts +17 -0
- package/dist/src/hooks/schema-drift.js +151 -0
- package/dist/src/hooks/schema-drift.js.map +1 -0
- package/dist/src/hooks/shared.d.ts +43 -0
- package/dist/src/hooks/shared.js +98 -0
- package/dist/src/hooks/shared.js.map +1 -0
- package/dist/src/init.d.ts +20 -0
- package/dist/src/init.js +193 -0
- package/dist/src/init.js.map +1 -0
- package/dist/src/preview/mockup-generator.d.ts +56 -0
- package/dist/src/preview/mockup-generator.js +402 -0
- package/dist/src/preview/mockup-generator.js.map +1 -0
- package/dist/src/preview/renderer.d.ts +30 -0
- package/dist/src/preview/renderer.js +145 -0
- package/dist/src/preview/renderer.js.map +1 -0
- package/dist/src/preview/server.d.ts +9 -0
- package/dist/src/preview/server.js +55 -0
- package/dist/src/preview/server.js.map +1 -0
- package/dist/src/preview/ui-auditor.d.ts +27 -0
- package/dist/src/preview/ui-auditor.js +141 -0
- package/dist/src/preview/ui-auditor.js.map +1 -0
- package/dist/src/preview/ui-gate.d.ts +66 -0
- package/dist/src/preview/ui-gate.js +210 -0
- package/dist/src/preview/ui-gate.js.map +1 -0
- package/dist/src/utils/ascii.d.ts +7 -0
- package/dist/src/utils/ascii.js +41 -0
- package/dist/src/utils/ascii.js.map +1 -0
- package/dist/src/utils/fs.d.ts +6 -0
- package/dist/src/utils/fs.js +39 -0
- package/dist/src/utils/fs.js.map +1 -0
- package/dist/templates/hooks/iddd-auto-audit.js +121 -0
- package/dist/templates/hooks/iddd-schema-drift.js +279 -0
- package/dist/templates/hooks/post-commit +121 -0
- package/dist/templates/hooks/pre-commit +348 -0
- package/package.json +37 -0
- package/templates/.agents/skills/.gitkeep +0 -0
- package/templates/.claude/hooks/.gitkeep +0 -0
- package/templates/.claude/hooks/hook-config.json +34 -0
- package/templates/.claude/skills/.gitkeep +0 -0
- package/templates/.codex/.gitkeep +0 -0
- package/templates/.codex/hooks.json +40 -0
- package/templates/.iddd/commit-count +1 -0
- package/templates/.iddd/preview/.gitkeep +0 -0
- package/templates/AGENTS.md +204 -0
- package/templates/CLAUDE.md +215 -0
- package/templates/README.md +476 -0
- package/templates/docs/.gitkeep +0 -0
- package/templates/docs/business-rules.md +14 -0
- package/templates/docs/domain-glossary.md +8 -0
- package/templates/docs/info-debt.md +17 -0
- package/templates/docs/model-changelog.md +12 -0
- package/templates/hooks/.gitkeep +0 -0
- package/templates/hooks/iddd-auto-audit.js +121 -0
- package/templates/hooks/iddd-schema-drift.js +279 -0
- package/templates/hooks/post-commit +121 -0
- package/templates/hooks/pre-commit +348 -0
- package/templates/skills/id3-design-information/SKILL.md +170 -0
- package/templates/skills/id3-design-information/references/phase2-procedure.md +241 -0
- package/templates/skills/id3-design-ui/.gitkeep +0 -0
- package/templates/skills/id3-design-ui/SKILL.md +200 -0
- package/templates/skills/id3-design-ui/references/.gitkeep +0 -0
- package/templates/skills/id3-design-ui/references/step1-structure-derivation.md +177 -0
- package/templates/skills/id3-design-ui/references/step2-visual-contract.md +257 -0
- package/templates/skills/id3-design-ui/references/step3-gate-and-mockup.md +177 -0
- package/templates/skills/id3-design-ui/references/step4-implementation.md +244 -0
- package/templates/skills/id3-identify-entities/SKILL.md +239 -0
- package/templates/skills/id3-identify-entities/references/.gitkeep +0 -0
- package/templates/skills/id3-identify-entities/references/phase0-brownfield.md +377 -0
- package/templates/skills/id3-identify-entities/references/phase1-greenfield.md +319 -0
- package/templates/skills/id3-info-audit/.gitkeep +0 -0
- package/templates/skills/id3-info-audit/SKILL.md +191 -0
- package/templates/skills/id3-preview/.gitkeep +0 -0
- package/templates/skills/id3-preview/SKILL.md +168 -0
- package/templates/skills/id3-spawn-team/.gitkeep +0 -0
- package/templates/skills/id3-spawn-team/SKILL.md +213 -0
- package/templates/specs/.gitkeep +0 -0
- package/templates/specs/data-model.md +26 -0
- package/templates/specs/entity-catalog.md +22 -0
- package/templates/specs/ui-design-contract.md +54 -0
- package/templates/specs/ui-inventory.md +24 -0
- package/templates/specs/ui-structure.md +32 -0
- package/templates/src/.gitkeep +0 -0
- package/templates/steering/.gitkeep +0 -0
- package/templates/steering/data-conventions.md +42 -0
- package/templates/steering/product.md +38 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Business Rules
|
|
2
|
+
|
|
3
|
+
> These rules are automatically derived from the information model (entity-catalog.md).
|
|
4
|
+
> Rule numbers are assigned sequentially starting from BR-001.
|
|
5
|
+
|
|
6
|
+
## Rule Summary Matrix
|
|
7
|
+
|
|
8
|
+
| Entity | Applicable Rules |
|
|
9
|
+
|--------|------------------|
|
|
10
|
+
| _(Populated in Phase 2)_ | |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
_(Detailed definitions for each rule will be added below)_
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Domain Glossary (Ubiquitous Language)
|
|
2
|
+
|
|
3
|
+
> A term dictionary used consistently across the entire team.
|
|
4
|
+
> Variable names in code, field names in APIs, and terms in documentation must all align with this glossary.
|
|
5
|
+
|
|
6
|
+
| Term | English / Code | Definition | Notes |
|
|
7
|
+
|------|----------------|------------|-------|
|
|
8
|
+
| _(Populated in Phase 1)_ | | | |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Information Debt Ledger
|
|
2
|
+
|
|
3
|
+
> For brownfield projects, this records information-design technical debt discovered during Phase 0 (reverse extraction).
|
|
4
|
+
> For greenfield projects, it is normal for this file to be empty.
|
|
5
|
+
|
|
6
|
+
## Discrepancy Items
|
|
7
|
+
|
|
8
|
+
| ID | Type | Description | Related Entity | Severity | Status |
|
|
9
|
+
|----|------|-------------|----------------|----------|--------|
|
|
10
|
+
| _(Populated in Phase 0)_ | | | | | |
|
|
11
|
+
|
|
12
|
+
## Type Classification
|
|
13
|
+
|
|
14
|
+
- **Cross-layer mismatch**: Information representation differs between DB schema and ORM/API/frontend
|
|
15
|
+
- **Missing constraint**: Rules that exist only in code logic and are not reflected in the DB
|
|
16
|
+
- **Unused schema**: Tables or columns that exist in the DB but are not used in code
|
|
17
|
+
- **Contradictory rules**: Conflicting rules across different layers
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Information Model Changelog
|
|
2
|
+
|
|
3
|
+
> Change history for entity-catalog.md and data-model.md.
|
|
4
|
+
> Follows the [Keep a Changelog](https://keepachangelog.com) format.
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
_(Changes currently in progress)_
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
_(Version-specific change history will be added below)_
|
|
File without changes
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/claude-stop-entry.ts
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/hooks/auto-audit.ts
|
|
7
|
+
import { readFile as readFile2, writeFile, mkdir } from "node:fs/promises";
|
|
8
|
+
import { join as join2 } from "node:path";
|
|
9
|
+
|
|
10
|
+
// src/hooks/shared.ts
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
// src/utils/ascii.ts
|
|
16
|
+
function box(content, options = {}) {
|
|
17
|
+
const { title, width = 47, padding = 1 } = options;
|
|
18
|
+
const lines = content.split("\n");
|
|
19
|
+
const innerWidth = width - 2;
|
|
20
|
+
const padStr = " ".repeat(padding);
|
|
21
|
+
const result = [];
|
|
22
|
+
if (title) {
|
|
23
|
+
const titleStr = ` ${title} `;
|
|
24
|
+
const remaining = innerWidth - titleStr.length - 1;
|
|
25
|
+
result.push(` \u250C\u2500${titleStr}${"\u2500".repeat(Math.max(0, remaining))}\u2510`);
|
|
26
|
+
} else {
|
|
27
|
+
result.push(` \u250C${"\u2500".repeat(innerWidth)}\u2510`);
|
|
28
|
+
}
|
|
29
|
+
result.push(` \u2502${" ".repeat(innerWidth)}\u2502`);
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const padded = `${padStr}${line}`;
|
|
32
|
+
const spaces = innerWidth - padded.length;
|
|
33
|
+
result.push(` \u2502${padded}${" ".repeat(Math.max(0, spaces))}\u2502`);
|
|
34
|
+
}
|
|
35
|
+
result.push(` \u2502${" ".repeat(innerWidth)}\u2502`);
|
|
36
|
+
result.push(` \u2514${"\u2500".repeat(innerWidth)}\u2518`);
|
|
37
|
+
return result.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/hooks/shared.ts
|
|
41
|
+
async function loadHookConfig(projectRoot2) {
|
|
42
|
+
try {
|
|
43
|
+
const configPath = join(
|
|
44
|
+
projectRoot2,
|
|
45
|
+
".claude",
|
|
46
|
+
"hooks",
|
|
47
|
+
"hook-config.json"
|
|
48
|
+
);
|
|
49
|
+
const content = await readFile(configPath, "utf-8");
|
|
50
|
+
return JSON.parse(content);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isSkipHooks() {
|
|
56
|
+
return process.env["IDDD_SKIP_HOOKS"] === "1";
|
|
57
|
+
}
|
|
58
|
+
async function logSkip(projectRoot2) {
|
|
59
|
+
const { appendFile, mkdir: mkdir2 } = await import("node:fs/promises");
|
|
60
|
+
const logDir = join(projectRoot2, ".iddd");
|
|
61
|
+
await mkdir2(logDir, { recursive: true });
|
|
62
|
+
const logPath = join(logDir, "skip-history.log");
|
|
63
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
64
|
+
let commitHash = "unknown";
|
|
65
|
+
try {
|
|
66
|
+
commitHash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
await appendFile(logPath, `${timestamp} ${commitHash} Hook skipped
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
function printWarn(title, message) {
|
|
73
|
+
console.error(box(message, { title }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/hooks/auto-audit.ts
|
|
77
|
+
async function incrementCounter(projectRoot2) {
|
|
78
|
+
const counterPath = join2(projectRoot2, ".iddd", "commit-count");
|
|
79
|
+
let count = 0;
|
|
80
|
+
try {
|
|
81
|
+
const content = await readFile2(counterPath, "utf-8");
|
|
82
|
+
count = parseInt(content.trim(), 10) || 0;
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
count++;
|
|
86
|
+
await mkdir(join2(projectRoot2, ".iddd"), { recursive: true });
|
|
87
|
+
await writeFile(counterPath, count.toString());
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
async function resetCounter(projectRoot2) {
|
|
91
|
+
const counterPath = join2(projectRoot2, ".iddd", "commit-count");
|
|
92
|
+
await writeFile(counterPath, "0");
|
|
93
|
+
}
|
|
94
|
+
async function runAutoAudit(projectRoot2) {
|
|
95
|
+
if (isSkipHooks()) {
|
|
96
|
+
await logSkip(projectRoot2);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const config = await loadHookConfig(projectRoot2);
|
|
100
|
+
if (!config?.hooks["post-commit"]["auto-audit"]?.enabled) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const threshold = config.hooks["post-commit"]["auto-audit"].interval_commits;
|
|
104
|
+
const count = await incrementCounter(projectRoot2);
|
|
105
|
+
if (count >= threshold) {
|
|
106
|
+
printWarn(
|
|
107
|
+
"\u2139\uFE0F Auto-Audit Triggered",
|
|
108
|
+
`${count} commits since last audit.
|
|
109
|
+
Run /id3-info-audit for a full check.
|
|
110
|
+
|
|
111
|
+
Report saved to .iddd/last-audit-report.md`
|
|
112
|
+
);
|
|
113
|
+
await resetCounter(projectRoot2);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/hooks/claude-stop-entry.ts
|
|
118
|
+
var projectRoot = resolve(".");
|
|
119
|
+
runAutoAudit(projectRoot).catch((err) => {
|
|
120
|
+
console.error("IDDD claude-stop hook error:", err);
|
|
121
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/claude-pretool-entry.ts
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/hooks/shared.ts
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
// src/utils/ascii.ts
|
|
12
|
+
function box(content, options = {}) {
|
|
13
|
+
const { title, width = 47, padding = 1 } = options;
|
|
14
|
+
const lines = content.split("\n");
|
|
15
|
+
const innerWidth = width - 2;
|
|
16
|
+
const padStr = " ".repeat(padding);
|
|
17
|
+
const result = [];
|
|
18
|
+
if (title) {
|
|
19
|
+
const titleStr = ` ${title} `;
|
|
20
|
+
const remaining = innerWidth - titleStr.length - 1;
|
|
21
|
+
result.push(` \u250C\u2500${titleStr}${"\u2500".repeat(Math.max(0, remaining))}\u2510`);
|
|
22
|
+
} else {
|
|
23
|
+
result.push(` \u250C${"\u2500".repeat(innerWidth)}\u2510`);
|
|
24
|
+
}
|
|
25
|
+
result.push(` \u2502${" ".repeat(innerWidth)}\u2502`);
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const padded = `${padStr}${line}`;
|
|
28
|
+
const spaces = innerWidth - padded.length;
|
|
29
|
+
result.push(` \u2502${padded}${" ".repeat(Math.max(0, spaces))}\u2502`);
|
|
30
|
+
}
|
|
31
|
+
result.push(` \u2502${" ".repeat(innerWidth)}\u2502`);
|
|
32
|
+
result.push(` \u2514${"\u2500".repeat(innerWidth)}\u2518`);
|
|
33
|
+
return result.join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/hooks/shared.ts
|
|
37
|
+
function parseGitDiff(output) {
|
|
38
|
+
if (!output.trim()) return [];
|
|
39
|
+
return output.trim().split("\n").map((line) => {
|
|
40
|
+
const [status, path] = line.split(" ");
|
|
41
|
+
return { status, path };
|
|
42
|
+
}).filter((e) => e.path);
|
|
43
|
+
}
|
|
44
|
+
function parseHookInput(stdinData) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(stdinData);
|
|
47
|
+
if (!parsed.tool_name || !parsed.tool_input?.file_path) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
toolName: parsed.tool_name,
|
|
52
|
+
filePath: parsed.tool_input.file_path
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getCachedDiff() {
|
|
59
|
+
try {
|
|
60
|
+
const output = execSync("git diff --cached --name-status", {
|
|
61
|
+
encoding: "utf-8"
|
|
62
|
+
});
|
|
63
|
+
return parseGitDiff(output);
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function getCachedFileContent(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
return execSync(`git show :${filePath}`, { encoding: "utf-8" });
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function isSchemaFile(filePath, patterns) {
|
|
76
|
+
return patterns.some((pattern) => matchGlob(filePath, pattern));
|
|
77
|
+
}
|
|
78
|
+
function isValidationFile(filePath, patterns) {
|
|
79
|
+
return patterns.some((pattern) => matchGlob(filePath, pattern));
|
|
80
|
+
}
|
|
81
|
+
function matchGlob(filePath, pattern) {
|
|
82
|
+
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
83
|
+
return new RegExp(`^${regex}$`).test(filePath) || new RegExp(`(^|/)${regex}$`).test(filePath);
|
|
84
|
+
}
|
|
85
|
+
async function loadHookConfig(projectRoot2) {
|
|
86
|
+
try {
|
|
87
|
+
const configPath = join(
|
|
88
|
+
projectRoot2,
|
|
89
|
+
".claude",
|
|
90
|
+
"hooks",
|
|
91
|
+
"hook-config.json"
|
|
92
|
+
);
|
|
93
|
+
const content = await readFile(configPath, "utf-8");
|
|
94
|
+
return JSON.parse(content);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function isSkipHooks() {
|
|
100
|
+
return process.env["IDDD_SKIP_HOOKS"] === "1";
|
|
101
|
+
}
|
|
102
|
+
async function logSkip(projectRoot2) {
|
|
103
|
+
const { appendFile, mkdir } = await import("node:fs/promises");
|
|
104
|
+
const logDir = join(projectRoot2, ".iddd");
|
|
105
|
+
await mkdir(logDir, { recursive: true });
|
|
106
|
+
const logPath = join(logDir, "skip-history.log");
|
|
107
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
108
|
+
let commitHash = "unknown";
|
|
109
|
+
try {
|
|
110
|
+
commitHash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
await appendFile(logPath, `${timestamp} ${commitHash} Hook skipped
|
|
114
|
+
`);
|
|
115
|
+
}
|
|
116
|
+
function printWarn(title, message) {
|
|
117
|
+
console.error(box(message, { title }));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/hooks/schema-drift.ts
|
|
121
|
+
async function runSchemaDriftFromHookInput(projectRoot2, hookInput) {
|
|
122
|
+
if (isSkipHooks()) {
|
|
123
|
+
await logSkip(projectRoot2);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
const config = await loadHookConfig(projectRoot2);
|
|
127
|
+
if (!config?.hooks["pre-commit"]["schema-drift"]?.enabled) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const { monitored_patterns } = config.hooks["pre-commit"]["schema-drift"];
|
|
131
|
+
const relativePath = hookInput.filePath.startsWith(projectRoot2) ? hookInput.filePath.slice(projectRoot2.length + 1) : hookInput.filePath;
|
|
132
|
+
if (!isSchemaFile(relativePath, monitored_patterns)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
printWarn(
|
|
136
|
+
"\u26A0\uFE0F Schema File Modified",
|
|
137
|
+
`File: ${relativePath}
|
|
138
|
+
|
|
139
|
+
Ensure specs/entity-catalog.md is updated
|
|
140
|
+
to reflect this schema change.`
|
|
141
|
+
);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/hooks/rule-check.ts
|
|
146
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
147
|
+
import { join as join2 } from "node:path";
|
|
148
|
+
var VALIDATION_PATTERNS = [
|
|
149
|
+
{
|
|
150
|
+
regex: /z\.\s*(?:object|string|number|boolean|array|enum)\s*\(/g,
|
|
151
|
+
label: "zod"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
regex: /yup\.\s*(?:object|string|number|boolean|array)\s*\(/g,
|
|
155
|
+
label: "yup"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
regex: /Joi\.\s*(?:object|string|number|boolean|array)\s*\(/g,
|
|
159
|
+
label: "joi"
|
|
160
|
+
},
|
|
161
|
+
{ regex: /CHECK\s*\(/gi, label: "SQL CHECK" },
|
|
162
|
+
{ regex: /NOT\s+NULL/gi, label: "SQL NOT NULL" },
|
|
163
|
+
{ regex: /ADD\s+.*UNIQUE/gi, label: "SQL UNIQUE" },
|
|
164
|
+
{ regex: /@validator\s*\(/g, label: "pydantic" },
|
|
165
|
+
{ regex: /@field_validator\s*\(/g, label: "pydantic-v2" },
|
|
166
|
+
{ regex: /@Valid/g, label: "java-valid" },
|
|
167
|
+
{ regex: /@NotNull/g, label: "java-notnull" },
|
|
168
|
+
{
|
|
169
|
+
regex: /@Column\s*\(\s*.*nullable\s*[:=]\s*false/g,
|
|
170
|
+
label: "orm-notnull"
|
|
171
|
+
},
|
|
172
|
+
{ regex: /@IsNotEmpty\s*\(/g, label: "class-validator" },
|
|
173
|
+
{
|
|
174
|
+
regex: /body\s*\(\s*['"].*['"]\s*\)\s*\.(?:not|is)/g,
|
|
175
|
+
label: "express-validator"
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
function detectValidationPatterns(content, filePath) {
|
|
179
|
+
const detections = [];
|
|
180
|
+
const lines = content.split("\n");
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
const line = lines[i];
|
|
183
|
+
if (!line.startsWith("+") && content.includes("\n+")) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const cleanLine = line.replace(/^\+/, "");
|
|
187
|
+
for (const { regex, label } of VALIDATION_PATTERNS) {
|
|
188
|
+
regex.lastIndex = 0;
|
|
189
|
+
if (regex.test(cleanLine)) {
|
|
190
|
+
detections.push({
|
|
191
|
+
file: filePath,
|
|
192
|
+
line: i + 1,
|
|
193
|
+
content: cleanLine.trim(),
|
|
194
|
+
pattern: label
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return detections;
|
|
200
|
+
}
|
|
201
|
+
async function hasMatchingRule(projectRoot2, detection) {
|
|
202
|
+
try {
|
|
203
|
+
const rulesPath = join2(projectRoot2, "docs", "business-rules.md");
|
|
204
|
+
const rules = await readFile2(rulesPath, "utf-8");
|
|
205
|
+
return rules.includes("BR-") && rules.length > 200;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function runRuleCheck(projectRoot2) {
|
|
211
|
+
if (isSkipHooks()) {
|
|
212
|
+
await logSkip(projectRoot2);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
const config = await loadHookConfig(projectRoot2);
|
|
216
|
+
if (!config?.hooks["pre-commit"]["rule-check"]?.enabled) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const { validation_patterns } = config.hooks["pre-commit"]["rule-check"];
|
|
220
|
+
const diff = getCachedDiff();
|
|
221
|
+
const validationFiles = diff.filter(
|
|
222
|
+
(e) => isValidationFile(e.path, validation_patterns)
|
|
223
|
+
);
|
|
224
|
+
if (validationFiles.length === 0) return true;
|
|
225
|
+
const allDetections = [];
|
|
226
|
+
for (const file of validationFiles) {
|
|
227
|
+
const content = getCachedFileContent(file.path);
|
|
228
|
+
if (content) {
|
|
229
|
+
allDetections.push(...detectValidationPatterns(content, file.path));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (allDetections.length === 0) return true;
|
|
233
|
+
for (const detection of allDetections) {
|
|
234
|
+
const matched = await hasMatchingRule(projectRoot2, detection);
|
|
235
|
+
if (!matched) {
|
|
236
|
+
printWarn(
|
|
237
|
+
"\u26A0\uFE0F New Validation Detected",
|
|
238
|
+
`File: ${detection.file}:${detection.line}
|
|
239
|
+
Pattern: ${detection.pattern}
|
|
240
|
+
Content: ${detection.content}
|
|
241
|
+
|
|
242
|
+
No matching BR-xxx in business-rules.md
|
|
243
|
+
Consider registering this rule.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/hooks/claude-pretool-entry.ts
|
|
251
|
+
var projectRoot = resolve(".");
|
|
252
|
+
async function main() {
|
|
253
|
+
const chunks = [];
|
|
254
|
+
for await (const chunk of process.stdin) {
|
|
255
|
+
chunks.push(chunk);
|
|
256
|
+
}
|
|
257
|
+
const stdinData = Buffer.concat(chunks).toString("utf-8");
|
|
258
|
+
const hookInput = parseHookInput(stdinData);
|
|
259
|
+
if (!hookInput) {
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
const schemaDriftOk = await runSchemaDriftFromHookInput(
|
|
263
|
+
projectRoot,
|
|
264
|
+
hookInput
|
|
265
|
+
);
|
|
266
|
+
if (!schemaDriftOk) {
|
|
267
|
+
const result = {
|
|
268
|
+
decision: "block",
|
|
269
|
+
reason: "Schema drift detected. Update specs/entity-catalog.md first."
|
|
270
|
+
};
|
|
271
|
+
process.stdout.write(JSON.stringify(result));
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
await runRuleCheck(projectRoot);
|
|
275
|
+
}
|
|
276
|
+
main().catch((err) => {
|
|
277
|
+
console.error("IDDD claude-pretool hook error:", err);
|
|
278
|
+
process.exit(0);
|
|
279
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hooks/post-commit-entry.ts
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/hooks/auto-audit.ts
|
|
7
|
+
import { readFile as readFile2, writeFile, mkdir } from "node:fs/promises";
|
|
8
|
+
import { join as join2 } from "node:path";
|
|
9
|
+
|
|
10
|
+
// src/hooks/shared.ts
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
// src/utils/ascii.ts
|
|
16
|
+
function box(content, options = {}) {
|
|
17
|
+
const { title, width = 47, padding = 1 } = options;
|
|
18
|
+
const lines = content.split("\n");
|
|
19
|
+
const innerWidth = width - 2;
|
|
20
|
+
const padStr = " ".repeat(padding);
|
|
21
|
+
const result = [];
|
|
22
|
+
if (title) {
|
|
23
|
+
const titleStr = ` ${title} `;
|
|
24
|
+
const remaining = innerWidth - titleStr.length - 1;
|
|
25
|
+
result.push(` \u250C\u2500${titleStr}${"\u2500".repeat(Math.max(0, remaining))}\u2510`);
|
|
26
|
+
} else {
|
|
27
|
+
result.push(` \u250C${"\u2500".repeat(innerWidth)}\u2510`);
|
|
28
|
+
}
|
|
29
|
+
result.push(` \u2502${" ".repeat(innerWidth)}\u2502`);
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const padded = `${padStr}${line}`;
|
|
32
|
+
const spaces = innerWidth - padded.length;
|
|
33
|
+
result.push(` \u2502${padded}${" ".repeat(Math.max(0, spaces))}\u2502`);
|
|
34
|
+
}
|
|
35
|
+
result.push(` \u2502${" ".repeat(innerWidth)}\u2502`);
|
|
36
|
+
result.push(` \u2514${"\u2500".repeat(innerWidth)}\u2518`);
|
|
37
|
+
return result.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/hooks/shared.ts
|
|
41
|
+
async function loadHookConfig(projectRoot2) {
|
|
42
|
+
try {
|
|
43
|
+
const configPath = join(
|
|
44
|
+
projectRoot2,
|
|
45
|
+
".claude",
|
|
46
|
+
"hooks",
|
|
47
|
+
"hook-config.json"
|
|
48
|
+
);
|
|
49
|
+
const content = await readFile(configPath, "utf-8");
|
|
50
|
+
return JSON.parse(content);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isSkipHooks() {
|
|
56
|
+
return process.env["IDDD_SKIP_HOOKS"] === "1";
|
|
57
|
+
}
|
|
58
|
+
async function logSkip(projectRoot2) {
|
|
59
|
+
const { appendFile, mkdir: mkdir2 } = await import("node:fs/promises");
|
|
60
|
+
const logDir = join(projectRoot2, ".iddd");
|
|
61
|
+
await mkdir2(logDir, { recursive: true });
|
|
62
|
+
const logPath = join(logDir, "skip-history.log");
|
|
63
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
64
|
+
let commitHash = "unknown";
|
|
65
|
+
try {
|
|
66
|
+
commitHash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
await appendFile(logPath, `${timestamp} ${commitHash} Hook skipped
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
function printWarn(title, message) {
|
|
73
|
+
console.error(box(message, { title }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/hooks/auto-audit.ts
|
|
77
|
+
async function incrementCounter(projectRoot2) {
|
|
78
|
+
const counterPath = join2(projectRoot2, ".iddd", "commit-count");
|
|
79
|
+
let count = 0;
|
|
80
|
+
try {
|
|
81
|
+
const content = await readFile2(counterPath, "utf-8");
|
|
82
|
+
count = parseInt(content.trim(), 10) || 0;
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
count++;
|
|
86
|
+
await mkdir(join2(projectRoot2, ".iddd"), { recursive: true });
|
|
87
|
+
await writeFile(counterPath, count.toString());
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
async function resetCounter(projectRoot2) {
|
|
91
|
+
const counterPath = join2(projectRoot2, ".iddd", "commit-count");
|
|
92
|
+
await writeFile(counterPath, "0");
|
|
93
|
+
}
|
|
94
|
+
async function runAutoAudit(projectRoot2) {
|
|
95
|
+
if (isSkipHooks()) {
|
|
96
|
+
await logSkip(projectRoot2);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const config = await loadHookConfig(projectRoot2);
|
|
100
|
+
if (!config?.hooks["post-commit"]["auto-audit"]?.enabled) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const threshold = config.hooks["post-commit"]["auto-audit"].interval_commits;
|
|
104
|
+
const count = await incrementCounter(projectRoot2);
|
|
105
|
+
if (count >= threshold) {
|
|
106
|
+
printWarn(
|
|
107
|
+
"\u2139\uFE0F Auto-Audit Triggered",
|
|
108
|
+
`${count} commits since last audit.
|
|
109
|
+
Run /id3-info-audit for a full check.
|
|
110
|
+
|
|
111
|
+
Report saved to .iddd/last-audit-report.md`
|
|
112
|
+
);
|
|
113
|
+
await resetCounter(projectRoot2);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/hooks/post-commit-entry.ts
|
|
118
|
+
var projectRoot = resolve(".");
|
|
119
|
+
runAutoAudit(projectRoot).catch((err) => {
|
|
120
|
+
console.error("IDDD post-commit hook error:", err);
|
|
121
|
+
});
|