azcodr 2.3.0 → 2.5.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.
Files changed (45) hide show
  1. package/.agents/hooks.json +15 -0
  2. package/.agents/hooks.json.example +15 -0
  3. package/.agents/lib/agent-guard-command.js +103 -0
  4. package/.agents/lib/agent-guard-file.js +77 -0
  5. package/.agents/lib/agent-guard-tdd.js +77 -0
  6. package/.agents/lib/agent-guard.js +80 -0
  7. package/.agents/scripts/agent_guard.js +35 -3
  8. package/.agents/skills/clean-code-refactor/SKILL.md +1 -0
  9. package/.agents/skills/lets-build/SKILL.md +3 -2
  10. package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +159 -0
  11. package/.agents/skills/relentless-questioner/SKILL.md +1 -1
  12. package/.github/workflows/ci.yml +13 -0
  13. package/.github/workflows/publish.yml +7 -0
  14. package/AGENTS.md +3 -3
  15. package/CODE_OF_CONDUCT.md +122 -0
  16. package/CONTRIBUTING.md +16 -4
  17. package/README.md +77 -4
  18. package/docs/generated-enforcement-analysis.md +141 -0
  19. package/docs/rules/clean_code.md +3 -2
  20. package/docs/rules/test_driven_development.md +8 -5
  21. package/lib/cli-boundaries.d.ts +18 -0
  22. package/lib/cli-boundaries.d.ts.map +1 -0
  23. package/lib/cli-boundaries.js +46 -0
  24. package/lib/cli-boundaries.js.map +1 -0
  25. package/lib/cli-parse.d.ts +2 -1
  26. package/lib/cli-parse.d.ts.map +1 -1
  27. package/lib/cli-parse.js +15 -3
  28. package/lib/cli-parse.js.map +1 -1
  29. package/lib/cli.d.ts +3 -0
  30. package/lib/cli.d.ts.map +1 -1
  31. package/lib/cli.js +18 -4
  32. package/lib/cli.js.map +1 -1
  33. package/lib/index.d.ts +1 -1
  34. package/lib/index.js +1 -1
  35. package/lib/scaffold.js +1 -1
  36. package/lib/scaffold.js.map +1 -1
  37. package/memory.md +32 -0
  38. package/package.json +9 -3
  39. package/scripts/validate/toolchain.js +60 -0
  40. package/scripts/validate.js +12 -2
  41. package/src/cli-boundaries.ts +67 -0
  42. package/src/cli-parse.ts +19 -5
  43. package/src/cli.ts +20 -4
  44. package/src/index.ts +1 -1
  45. package/src/scaffold.ts +1 -1
@@ -14,6 +14,21 @@
14
14
  }
15
15
  ]
16
16
  },
17
+ "architectural-guard": {
18
+ "enabled": false,
19
+ "PreToolUse": [
20
+ {
21
+ "matcher": "run_command",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "./.agents/scripts/agent_guard.js",
26
+ "timeout": 15
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ },
17
32
  "post-tool-lint": {
18
33
  "enabled": false,
19
34
  "PostToolUse": [
@@ -14,6 +14,21 @@
14
14
  }
15
15
  ]
16
16
  },
17
+ "architectural-guard": {
18
+ "enabled": false,
19
+ "PreToolUse": [
20
+ {
21
+ "matcher": "run_command",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "./.agents/scripts/agent_guard.js",
26
+ "timeout": 15
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ },
17
32
  "post-tool-lint": {
18
33
  "enabled": false,
19
34
  "PostToolUse": [
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Command safety guard inspecting shell commands against a destructive denylist.
3
+ */
4
+ export const DESTRUCTIVE_RULES = [
5
+ {
6
+ pattern: /\brm\s+(-[a-z-]*[rf][a-z-]*\s+)+(\/|\/\*|~|~\/\*|\$home|\$\{home\}|\.|\.\.|\.\/|[a-z]:\\|[a-z]:\/)/i,
7
+ reason: 'destructive rm targeting root, home, drive, or the current directory'
8
+ },
9
+ {
10
+ pattern: /(^|\s)rm\s+(-[a-z-]*\s+)*(\/|~|[a-z]:\\|[a-z]:\/)($|\s)/i,
11
+ reason: 'destructive rm targeting root or home'
12
+ },
13
+ {
14
+ pattern: /\brm\s+--recursive.*--force\s+(\/|~|\$home|\.)/i,
15
+ reason: 'destructive rm using long-form flags'
16
+ },
17
+ {
18
+ pattern: /\brm\s+.*--no-preserve-root/i,
19
+ reason: 'rm --no-preserve-root is forbidden under agent execution'
20
+ },
21
+ {
22
+ pattern: /\b(wipefs\s+-a|shred\s+-u|find\s+.*-delete)\b/i,
23
+ reason: 'irreversible disk or mass-delete operation'
24
+ },
25
+ {
26
+ pattern: /\bgit\s+push\s+.*(-f|--force|--mirror)\b/i,
27
+ reason: 'force-push or mirror-push can destroy remote history'
28
+ },
29
+ {
30
+ pattern: /\bgit\s+push\b.*--force-with-lease\b/i,
31
+ reason: 'force-push variant detected'
32
+ },
33
+ {
34
+ pattern: /\bgit\s+(reset\s+--hard|clean\s+-[a-z]*[fd])/i,
35
+ reason: 'destructive git worktree operation (reset --hard / clean)'
36
+ },
37
+ {
38
+ pattern: /\bgit\s+checkout\s+--\s+\./i,
39
+ reason: 'git checkout -- . discards all uncommitted work'
40
+ },
41
+ {
42
+ pattern: /\bgit\s+branch\s+-D\b/i,
43
+ reason: 'force branch delete'
44
+ },
45
+ {
46
+ pattern: /\b(drop\s+(database|schema|table)|truncate\s+(table\s+)?[a-z_])/i,
47
+ reason: 'destructive SQL DDL'
48
+ },
49
+ {
50
+ pattern: /\b(delete\s+from|update\s+[a-z_]+\s+set|alter\s+table)\b/i,
51
+ reason: 'destructive SQL DML'
52
+ },
53
+ {
54
+ pattern: /\b(grant\s+all|revoke\s+all)\b/i,
55
+ reason: 'privilege escalation in SQL'
56
+ },
57
+ {
58
+ pattern: /\b(mkfs|dd\s+if=|fdisk|parted\s+\/dev)\b/i,
59
+ reason: 'raw disk operation'
60
+ },
61
+ {
62
+ pattern: /(:[\s]*\([\s]*\)[\s]*\{|:[\s]*\|[\s]*:)/,
63
+ reason: 'fork bomb'
64
+ },
65
+ {
66
+ pattern: /\b(curl|wget)\s+.*\|\s*(sudo\s+)?(ba)?sh\b/i,
67
+ reason: 'piping download directly into shell execution'
68
+ },
69
+ {
70
+ pattern: /\b(base64\s+-d|base64\s+--decode)\s+.*\|/i,
71
+ reason: 'decoding encoded payload into a pipe'
72
+ },
73
+ {
74
+ pattern: /\b(npm|yarn|pnpm)\s+publish\b/i,
75
+ reason: 'package publish must run via release workflow, not ad-hoc tool call'
76
+ }
77
+ ];
78
+ export function normalizeCommand(raw) {
79
+ return raw.replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim();
80
+ }
81
+ /**
82
+ * Checks a command string against the safety denylist.
83
+ *
84
+ * @param command - Raw shell command string.
85
+ * @returns Violation details if blocked, or clean outcome.
86
+ */
87
+ export function inspectCommand(command) {
88
+ const normalized = normalizeCommand(command);
89
+ if (!normalized)
90
+ return { blocked: false };
91
+ for (const rule of DESTRUCTIVE_RULES) {
92
+ if (rule.pattern.test(normalized)) {
93
+ return {
94
+ blocked: true,
95
+ reason: rule.reason,
96
+ command: normalized.slice(0, 300)
97
+ };
98
+ }
99
+ }
100
+ return { blocked: false, command: normalized };
101
+ }
102
+ export default { DESTRUCTIVE_RULES, normalizeCommand, inspectCommand };
103
+ //# sourceMappingURL=agent-guard-command.js.map
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const DEFAULT_MAX_FILE_LINES = 300;
4
+ export function countLines(text) {
5
+ if (!text)
6
+ return 0;
7
+ return text.split('\n').length;
8
+ }
9
+ export function readExistingLines(filePath) {
10
+ if (!fs.existsSync(filePath))
11
+ return 0;
12
+ try {
13
+ const content = fs.readFileSync(filePath, 'utf-8');
14
+ return countLines(content);
15
+ }
16
+ catch {
17
+ return 0;
18
+ }
19
+ }
20
+ function calculateProjectedLines(currentLines, input) {
21
+ if (input.incomingContent !== undefined) {
22
+ return countLines(input.incomingContent);
23
+ }
24
+ if (input.replacementContent !== undefined) {
25
+ const addedLines = countLines(input.replacementContent);
26
+ const removedLines = input.targetContent !== undefined ? countLines(input.targetContent) : 1;
27
+ return Math.max(0, currentLines + addedLines - removedLines);
28
+ }
29
+ return currentLines;
30
+ }
31
+ function evaluateLineThresholds(params) {
32
+ const { filePath, currentLines, projectedLines, maxLines } = params;
33
+ const base = { currentLines, projectedLines, maxLines };
34
+ const relPath = path.basename(filePath);
35
+ if (currentLines > maxLines) {
36
+ if (projectedLines >= currentLines) {
37
+ return {
38
+ ...base,
39
+ blocked: true,
40
+ reason: `Refactor-Before-Add violation: '${relPath}' currently has ${currentLines} lines (limit: ${maxLines}). Adding or maintaining lines in an overflowing file is blocked. Refactor into modular units under green tests first.`
41
+ };
42
+ }
43
+ return { ...base, blocked: false };
44
+ }
45
+ if (projectedLines > maxLines) {
46
+ return {
47
+ ...base,
48
+ blocked: true,
49
+ reason: `File line budget exceeded: '${relPath}' would grow to ${projectedLines} lines (limit: ${maxLines}). Decompose into modular components before appending more code.`
50
+ };
51
+ }
52
+ return { ...base, blocked: false };
53
+ }
54
+ /**
55
+ * Checks a proposed file write or edit against Refactor-Before-Add rules.
56
+ *
57
+ * @param input - File path and proposed content.
58
+ * @returns Decision result indicating whether the write is blocked.
59
+ */
60
+ export function inspectFileWrite(input) {
61
+ const maxLines = input.maxLines ?? DEFAULT_MAX_FILE_LINES;
62
+ const currentLines = readExistingLines(input.filePath);
63
+ const projectedLines = calculateProjectedLines(currentLines, input);
64
+ return evaluateLineThresholds({
65
+ filePath: input.filePath,
66
+ currentLines,
67
+ projectedLines,
68
+ maxLines
69
+ });
70
+ }
71
+ export default {
72
+ DEFAULT_MAX_FILE_LINES,
73
+ countLines,
74
+ readExistingLines,
75
+ inspectFileWrite
76
+ };
77
+ //# sourceMappingURL=agent-guard-file.js.map
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function isTestFile(filePath) {
4
+ const norm = filePath.replace(/\\/g, '/').toLowerCase();
5
+ if (/(^|\/)(tests?|__tests__)(\/|$)/i.test(norm)) {
6
+ return true;
7
+ }
8
+ return /\.(test|spec)\.[a-z0-9]+$/i.test(norm);
9
+ }
10
+ export function isNonCodeFile(filePath) {
11
+ const ext = path.extname(filePath).toLowerCase();
12
+ const nonCodeExts = new Set(['.md', '.json', '.yml', '.yaml', '.txt', '.toml', '.lock', '.svg', '.png']);
13
+ return nonCodeExts.has(ext);
14
+ }
15
+ export function isProductionFile(filePath) {
16
+ if (isNonCodeFile(filePath))
17
+ return false;
18
+ if (isTestFile(filePath))
19
+ return false;
20
+ const norm = filePath.replace(/\\/g, '/').toLowerCase();
21
+ return norm.includes('src/') || norm.includes('lib/') || norm.includes('app/');
22
+ }
23
+ export function readTddState(stateFile) {
24
+ if (!fs.existsSync(stateFile)) {
25
+ return { lastFailingTestRecorded: false };
26
+ }
27
+ try {
28
+ const raw = fs.readFileSync(stateFile, 'utf-8');
29
+ return JSON.parse(raw);
30
+ }
31
+ catch {
32
+ return { lastFailingTestRecorded: false };
33
+ }
34
+ }
35
+ export function writeTddState(stateFile, state) {
36
+ try {
37
+ const dir = path.dirname(stateFile);
38
+ if (!fs.existsSync(dir))
39
+ fs.mkdirSync(dir, { recursive: true });
40
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf-8');
41
+ }
42
+ catch {
43
+ // Fail-safe write: do not crash agent if state cannot be persisted
44
+ }
45
+ }
46
+ /**
47
+ * Checks whether an edit to a file is allowed under RED-before-GREEN TDD rules.
48
+ *
49
+ * @param filePath - Path to file being edited.
50
+ * @param state - Current session TDD state.
51
+ * @param enforceTestFirst - Whether test-first enforcement is strictly enabled.
52
+ * @returns Decision result indicating whether edit is blocked.
53
+ */
54
+ export function inspectTddRequirement(filePath, state, enforceTestFirst = false) {
55
+ const prod = isProductionFile(filePath);
56
+ if (!enforceTestFirst || !prod) {
57
+ return { blocked: false, isProductionFile: prod };
58
+ }
59
+ if (!state.lastFailingTestRecorded) {
60
+ const baseName = path.basename(filePath);
61
+ return {
62
+ blocked: true,
63
+ isProductionFile: true,
64
+ reason: `Test-First (RED-before-GREEN) requirement: Production file '${baseName}' cannot be edited before a failing test (RED) is recorded. Author a failing unit or acceptance test first, verify failure output, then edit production code.`
65
+ };
66
+ }
67
+ return { blocked: false, isProductionFile: true };
68
+ }
69
+ export default {
70
+ isTestFile,
71
+ isNonCodeFile,
72
+ isProductionFile,
73
+ readTddState,
74
+ writeTddState,
75
+ inspectTddRequirement
76
+ };
77
+ //# sourceMappingURL=agent-guard-tdd.js.map
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { inspectCommand } from './agent-guard-command.js';
3
+ import { inspectFileWrite } from './agent-guard-file.js';
4
+ import { inspectTddRequirement, readTddState, writeTddState } from './agent-guard-tdd.js';
5
+ function parseEnvelope(raw) {
6
+ try {
7
+ const trimmed = raw.trim();
8
+ if (!trimmed.startsWith('{'))
9
+ return null;
10
+ return JSON.parse(trimmed);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function extractKey(input, keys) {
17
+ for (const k of keys) {
18
+ if (typeof input[k] === 'string')
19
+ return input[k];
20
+ }
21
+ return undefined;
22
+ }
23
+ function extractFilePath(input) {
24
+ return extractKey(input, ['TargetFile', 'file_path', 'path', 'filePath']);
25
+ }
26
+ function extractCommand(input) {
27
+ return extractKey(input, ['CommandLine', 'command', 'cmd']);
28
+ }
29
+ function inspectPreCommand(cmd) {
30
+ const violation = inspectCommand(cmd);
31
+ if (violation.blocked) {
32
+ return { allowed: false, reason: violation.reason, source: 'command' };
33
+ }
34
+ return { allowed: true };
35
+ }
36
+ function inspectPreFile(filePath, input, options) {
37
+ const incoming = extractKey(input, ['CodeContent', 'content']);
38
+ const replacement = extractKey(input, ['ReplacementContent', 'replacement']);
39
+ const target = extractKey(input, ['TargetContent', 'target']);
40
+ const fileDecision = inspectFileWrite({
41
+ filePath,
42
+ incomingContent: incoming,
43
+ replacementContent: replacement,
44
+ targetContent: target,
45
+ maxLines: options.maxFileLines
46
+ });
47
+ if (fileDecision.blocked) {
48
+ return { allowed: false, reason: fileDecision.reason, source: 'file' };
49
+ }
50
+ const root = options.workspaceRoot ?? process.cwd();
51
+ const statePath = path.join(root, '.agents', '.session-state.json');
52
+ const state = readTddState(statePath);
53
+ const tddDecision = inspectTddRequirement(filePath, state, options.enforceTestFirst);
54
+ if (tddDecision.blocked) {
55
+ return { allowed: false, reason: tddDecision.reason, source: 'tdd' };
56
+ }
57
+ return { allowed: true };
58
+ }
59
+ /**
60
+ * Inspects a tool-use request before execution (PreToolUse hook).
61
+ *
62
+ * @param rawInput - Raw command string, JSON envelope, or argv slice.
63
+ * @param options - Guard configuration options.
64
+ * @returns Guard decision indicating whether tool execution is permitted.
65
+ */
66
+ export function inspectPreTool(rawInput, options = {}) {
67
+ const env = parseEnvelope(rawInput);
68
+ if (env && env.tool_input) {
69
+ const cmd = extractCommand(env.tool_input);
70
+ if (cmd)
71
+ return inspectPreCommand(cmd);
72
+ const targetFile = extractFilePath(env.tool_input);
73
+ if (targetFile)
74
+ return inspectPreFile(targetFile, env.tool_input, options);
75
+ }
76
+ return inspectPreCommand(rawInput);
77
+ }
78
+ export { inspectCommand, inspectFileWrite, inspectTddRequirement, readTddState, writeTddState };
79
+ export default { inspectPreTool, inspectCommand, inspectFileWrite, inspectTddRequirement };
80
+ //# sourceMappingURL=agent-guard.js.map
@@ -14,10 +14,39 @@ import process from 'node:process';
14
14
  import { fileURLToPath, pathToFileURL } from 'node:url';
15
15
 
16
16
  const selfDir = path.dirname(fileURLToPath(import.meta.url));
17
- const guardModulePath = path.resolve(selfDir, '../../lib/agent-guard.js');
17
+
18
+ // Resolution order (first existing file wins):
19
+ // 1. AZCODR_GUARD_ENGINE override (tests, embedders).
20
+ // 2. ../lib/agent-guard.js — vendored engine shipped inside `.agents/`
21
+ // (present in scaffolded projects; `.agents` is copied recursively).
22
+ // 3. ../../lib/agent-guard.js — azcodr repo dev layout (fallback only).
23
+ const guardCandidates = [
24
+ process.env.AZCODR_GUARD_ENGINE,
25
+ path.resolve(selfDir, '../lib/agent-guard.js'),
26
+ path.resolve(selfDir, '../../lib/agent-guard.js')
27
+ ].filter(Boolean);
18
28
 
19
29
  async function getGuardModule() {
20
- return import(pathToFileURL(guardModulePath).href);
30
+ const tried = [];
31
+ for (const candidate of guardCandidates) {
32
+ if (!fs.existsSync(candidate)) {
33
+ tried.push(candidate);
34
+ continue;
35
+ }
36
+ try {
37
+ return await import(pathToFileURL(candidate).href);
38
+ } catch (err) {
39
+ tried.push(`${candidate} (unreadable: ${err.message})`);
40
+ }
41
+ }
42
+ // Fail CLOSED: a guard with no engine must be loud, never a silent allow.
43
+ // (Unparseable tool payloads still fail open inside inspectPreTool per
44
+ // ADR-005; a missing engine is a broken install, not an ambiguous input.)
45
+ console.error(
46
+ '🚨 Architectural Guard misconfigured: engine module not found, refusing to fail open. Tried:\n' +
47
+ tried.map((t) => ` - ${t}`).join('\n')
48
+ );
49
+ process.exit(2);
21
50
  }
22
51
 
23
52
  async function readStdin(timeoutMs = 1500) {
@@ -60,4 +89,7 @@ async function main() {
60
89
  process.exit(0);
61
90
  }
62
91
 
63
- main().catch(() => process.exit(0));
92
+ main().catch((err) => {
93
+ console.error(`🚨 Architectural Guard crashed: ${err?.message || err}`);
94
+ process.exit(2);
95
+ });
@@ -51,6 +51,7 @@ Target concrete flaws:
51
51
  ### Step 5: Verify Continuous Green State
52
52
  - Run tests after every single atomic change. Use the workspace's own command — do not assume one exists. Verify with the package manifest first (`node -p "JSON.stringify(require('./package.json').scripts)"`); for this template that is `npm test`, with `npm run test:coverage` for the coverage gate.
53
53
  - Ensure coverage remains at **100.00%**.
54
+ - Verify architectural boundaries: Run `npx azcodr boundaries` to guarantee the refactoring introduced 0 circular dependencies and 0 layer boundary breaches.
54
55
 
55
56
  ---
56
57
 
@@ -100,7 +100,7 @@ Upon user confirmation:
100
100
  - *Extension:* `manifest.json`, `src/background/index.ts`, `src/content/index.ts`, `src/popup/index.html`.
101
101
  - *Game / Engine:* `src/core/`, `src/ecs/`, asset manifest, frame loop entrypoint.
102
102
  - *CLI:* `src/cmd/`, `src/core/`, CLI entrypoint with exit code handling.
103
- 3. Generate build manifests (`Cargo.toml`, `package.json`, `go.mod`, `pyproject.toml`), deterministic linter configurations strictly enforcing the [Polyglot Fitness Function Standards](../../../docs/rules/clean_code.md#polyglot-fitness-function-standards), and boundary smoke test (`scripts/smoke_test.sh`).
103
+ 3. Generate build manifests (`Cargo.toml`, `package.json`, `go.mod`, `pyproject.toml`) for the recorded language, install the pinned linter from the emitted gate configuration (`eslint.config.js`, `ruff.toml`, `clippy.toml`, `.golangci.yml`, `checkstyle.xml`, `.editorconfig` CA block, `.clang-tidy` — written deterministically by `bootstrap_workspace.sh`, never invented). Node profiles arrive with `package.json` lint already rewired to `eslint .`; other profiles need the lint entry wired by hand. Prove the gate in Phase 5, and keep the boundary smoke test (`scripts/smoke_test.sh`).
104
104
  4. **Replace Starter README with Project-Specific README**:
105
105
  Generate a clean, project-specific `README.md` using [references/project_readme_template.md](./references/project_readme_template.md), completely replacing meta-template content with the project's actual name, mission, stack highlights, quickstart commands, and directory tree.
106
106
 
@@ -114,6 +114,7 @@ Upon user confirmation:
114
114
  2. Execute toolchain dependency checks, build commands, and health/smoke tests:
115
115
  - Compile code and verify zero compiler or lint errors.
116
116
  - Verify boundary verification smoke test (`scripts/smoke_test.sh`).
117
+ - **Prove enforcement is live:** enable the shipped hooks in `.agents/hooks.json` (`safety-guard`, `architectural-guard`), then deliberately attempt a blocked action (e.g. append lines to a file already over budget, or a denied command) and confirm the hook rejects it before re-disabling nothing — hooks stay enabled. A guard that has never blocked anything is a rumor, not a control.
117
118
  3. **Mandatory Handover to Domain Analysis (STOP & PIVOT):**
118
119
  - **`lets-build` IS NOW COMPLETE.** Do NOT proceed to write domain business entities, repositories, or application features.
119
120
  - Present the bootstrapped technical skeleton to the user.
@@ -160,7 +161,7 @@ Upon user confirmation:
160
161
 
161
162
  ## 4. Quality & Verification
162
163
  - **Testing Strategy:** Outside-In TDD with Nano-Cycles (Uncle Bob's 3 Laws)
163
- - **Code Health Gates:** 100.00% test coverage gate, zero lint errors
164
+ - **Code Health Gates:** 100% coverage on all metrics + mutation kill gate on guards, zero lint errors
164
165
  - **DevSecOps:** <Semgrep / Trivy / Gitleaks / None>
165
166
  ```
166
167
 
@@ -324,6 +324,165 @@ EOF
324
324
  ;;
325
325
  esac
326
326
 
327
+ # ------------------------------------------------------------------------------
328
+ # DETERMINISTIC TOOLCHAIN FILES. Directories alone enforce nothing: until this
329
+ # section existed, every language produced zero gate configurations and the
330
+ # starter `lint` script was an echo placeholder, so all enforcement was
331
+ # agent-authored. Each profile below emits its pinned gate configuration with
332
+ # values mirroring docs/rules/clean_code.md (300/30/10/3); the agent installs
333
+ # the named tool and proves the gate in Phase 5. Files are created only when
334
+ # absent, so re-runs never clobber agent-authored configs. Build manifests
335
+ # (Cargo.toml, go.mod, csproj, ...) stay with the agent: they carry project
336
+ # naming and version decisions no script may invent.
337
+ # ------------------------------------------------------------------------------
338
+ write_unless_exists() {
339
+ local target="$1"
340
+ if [[ -f "${target}" ]]; then
341
+ echo " keeping existing ${target}"
342
+ return
343
+ fi
344
+ mkdir -p "$(dirname "${target}")"
345
+ cat > "${target}"
346
+ }
347
+
348
+ append_unless_present() {
349
+ local target="$1"
350
+ local marker="$2"
351
+ if [[ -f "${target}" ]] && grep -qF "${marker}" "${target}"; then
352
+ echo " keeping existing ${target} gates"
353
+ return
354
+ fi
355
+ cat >> "${target}"
356
+ }
357
+
358
+ echo "6. Emitting deterministic toolchain configs for '${LANGUAGE}'..."
359
+ case "${LANGUAGE}" in
360
+ typescript|javascript|deno|bun)
361
+ write_unless_exists "${WORKSPACE_ROOT}/eslint.config.js" << 'EOF'
362
+ // Deterministic fitness functions (azcodr clean_code.md section 5).
363
+ // Install the pinned tool, then prove the gate: npm run lint
364
+ export default [
365
+ {
366
+ files: ['src/**/*.{js,ts}', 'tests/**/*.{js,ts}'],
367
+ rules: {
368
+ 'max-lines': ['error', 300],
369
+ 'max-lines-per-function': ['error', 30],
370
+ complexity: ['error', 10],
371
+ 'max-params': ['error', 3]
372
+ }
373
+ }
374
+ ];
375
+ EOF
376
+ ;;
377
+ python)
378
+ write_unless_exists "${WORKSPACE_ROOT}/ruff.toml" << 'EOF'
379
+ # Deterministic fitness functions (azcodr clean_code.md section 5).
380
+ # Install the pinned tool, then prove the gate: ruff check .
381
+ [lint]
382
+ select = ["E", "F", "C901", "PLR0912", "PLR0913", "PLR0915"]
383
+ [lint.mccabe]
384
+ max-complexity = 10
385
+ [lint.pylint]
386
+ max-args = 3
387
+ max-statements = 30
388
+ # NOTE: ruff has no file-length rule; the 300-line file cap is enforced by
389
+ # the project's lint entry (see lets-build Phase 5 proof).
390
+ EOF
391
+ ;;
392
+ rust)
393
+ write_unless_exists "${WORKSPACE_ROOT}/clippy.toml" << 'EOF'
394
+ # Deterministic fitness functions (azcodr clean_code.md section 5).
395
+ # Enforce with: cargo clippy -- -D clippy::too_many_lines -D clippy::cognitive_complexity -D clippy::too_many_arguments
396
+ too-many-lines-threshold = 30
397
+ cognitive-complexity-threshold = 10
398
+ too-many-arguments-threshold = 3
399
+ # NOTE: clippy has no file-length lint; the 300-line file cap is enforced by
400
+ # the project's lint entry (see lets-build Phase 5 proof).
401
+ EOF
402
+ ;;
403
+ go)
404
+ write_unless_exists "${WORKSPACE_ROOT}/.golangci.yml" << 'EOF'
405
+ # Deterministic fitness functions (azcodr clean_code.md section 5).
406
+ # Install the pinned tool, then prove the gate: golangci-lint run ./...
407
+ linters:
408
+ enable: [funlen, gocyclo]
409
+ linters-settings:
410
+ funlen:
411
+ lines: 30
412
+ statements: 25
413
+ gocyclo:
414
+ min-complexity: 10
415
+ # NOTE: no golangci-native file-length check; the 300-line file cap is
416
+ # enforced by the project's lint entry (see lets-build Phase 5 proof).
417
+ EOF
418
+ ;;
419
+ java)
420
+ write_unless_exists "${WORKSPACE_ROOT}/checkstyle.xml" << 'EOF'
421
+ <?xml version="1.0"?>
422
+ <!-- Deterministic fitness functions (azcodr clean_code.md section 5). -->
423
+ <!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
424
+ <module name="Checker">
425
+ <module name="FileLength">
426
+ <property name="max" value="300"/>
427
+ </module>
428
+ <module name="TreeWalker">
429
+ <module name="MethodLength">
430
+ <property name="max" value="30"/>
431
+ </module>
432
+ <module name="CyclomaticComplexity">
433
+ <property name="max" value="10"/>
434
+ </module>
435
+ <module name="ParameterNumber">
436
+ <property name="max" value="3"/>
437
+ </module>
438
+ </module>
439
+ </module>
440
+ EOF
441
+ ;;
442
+ csharp)
443
+ append_unless_present "${WORKSPACE_ROOT}/.editorconfig" "azcodr fitness functions" << 'EOF'
444
+
445
+ # --- azcodr fitness functions (CA gates; exact numbers in lint entry) ---
446
+ [*.cs]
447
+ dotnet_diagnostic.CA1501.severity = error
448
+ dotnet_diagnostic.CA1502.severity = error
449
+ EOF
450
+ ;;
451
+ cpp|c)
452
+ write_unless_exists "${WORKSPACE_ROOT}/.clang-tidy" << 'EOF'
453
+ # Deterministic fitness functions (azcodr clean_code.md section 5).
454
+ Checks: 'readability-function-size,readability-function-cognitive-complexity'
455
+ CheckOptions:
456
+ - { key: readability-function-size.LineThreshold, value: 30 }
457
+ - { key: readability-function-size.ParameterThreshold, value: 3 }
458
+ - { key: readability-function-cognitive-complexity.Threshold, value: 10 }
459
+ # NOTE: clang-tidy has no file-length check; the 300-line file cap is
460
+ # enforced by the project's lint entry (see lets-build Phase 5 proof).
461
+ EOF
462
+ ;;
463
+ generic)
464
+ echo " language undecided: no toolchain configs emitted (re-run with a language profile)"
465
+ ;;
466
+ esac
467
+
468
+ # For Node profiles the starter package.json ships a lint placeholder that
469
+ # always succeeds. Point it at the emitted config so `npm run lint` fails
470
+ # until the pinned eslint is installed (fail-closed) instead of echoing.
471
+ # Only the placeholder is ever replaced; agent-wired entries are sacred.
472
+ case "${LANGUAGE}" in
473
+ typescript|javascript|deno|bun)
474
+ STARTER_PKG="${WORKSPACE_ROOT}/package.json"
475
+ if [[ -f "${STARTER_PKG}" ]] && grep -q 'No linter configured yet' "${STARTER_PKG}"; then
476
+ if command -v node >/dev/null 2>&1; then
477
+ node -e 'const fs=require("fs");const p=process.argv[1];const j=JSON.parse(fs.readFileSync(p,"utf8"));j.scripts=j.scripts||{};j.scripts.lint="eslint .";fs.writeFileSync(p,JSON.stringify(j,null,2)+"\n");' "${STARTER_PKG}"
478
+ echo " wired package.json lint -> eslint ."
479
+ else
480
+ echo " node not found: leaving starter lint placeholder (agent wires it in Phase 4)"
481
+ fi
482
+ fi
483
+ ;;
484
+ esac
485
+
327
486
  # ------------------------------------------------------------------------------
328
487
  # memory.md is an APPEND-ONLY LEDGER. It is never rewritten automatically.
329
488
  #
@@ -119,7 +119,7 @@ Synthesize the answers into an unambiguous **Feature Alignment Specification (FA
119
119
  When [Action]
120
120
  Then [Observable Outcome]
121
121
  ```
122
- - **Test Strategy:** [Contract / Integration / Unit tests required for 100% coverage]
122
+ - **Test Strategy:** [Contract / Integration / Unit tests required; 100% on all metrics, assertion-free tests rejected]
123
123
  ```
124
124
 
125
125
  ---
@@ -54,6 +54,13 @@ jobs:
54
54
  node --version
55
55
  npm --version
56
56
 
57
+ - name: Verify TypeScript Compilation & lib/ Parity
58
+ shell: bash
59
+ run: |
60
+ npm run build
61
+ git diff --exit-code lib/
62
+ npm run typecheck
63
+
57
64
  - name: Run Syntax & Lint Checks
58
65
  run: npm run lint
59
66
 
@@ -84,6 +91,12 @@ jobs:
84
91
  - name: Verify Coverage Gates
85
92
  run: npm run test:coverage
86
93
 
94
+ - name: Verify Mutation Score Gate (100% Mutant Kill)
95
+ run: npm run test:mutation
96
+
97
+ - name: Verify Empirical Drift Benchmark
98
+ run: node benchmark/run-benchmark.js
99
+
87
100
  - name: Upload Coverage Report
88
101
  uses: actions/upload-artifact@v4
89
102
  if: always()