@mettlecast/domain-cli 0.2.66 → 0.2.68
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/dist/commands/doctor.js
CHANGED
|
@@ -726,6 +726,12 @@ async function checkAllHttpClientsUseKy(projectRoot) {
|
|
|
726
726
|
{ dir: join(projectRoot, 'domains'), label: 'domains' },
|
|
727
727
|
{ dir: join(projectRoot, 'packages', 'domain-cli', 'src', 'commands'), label: 'CLI commands' },
|
|
728
728
|
];
|
|
729
|
+
// Files to skip — these contain raw fetch() in comments, docs, or template
|
|
730
|
+
// literals that generate output code, not actual HTTP calls.
|
|
731
|
+
const skipFiles = new Set([
|
|
732
|
+
'generate-sdk.ts', // template literal generating SDK client code
|
|
733
|
+
'doctor.ts', // JSDoc block comments and fix-message strings self-flag
|
|
734
|
+
]);
|
|
729
735
|
const violations = [];
|
|
730
736
|
let totalFiles = 0;
|
|
731
737
|
for (const { dir, label } of scanDirs) {
|
|
@@ -734,6 +740,9 @@ async function checkAllHttpClientsUseKy(projectRoot) {
|
|
|
734
740
|
const files = findFiles(dir, /\.ts$/);
|
|
735
741
|
totalFiles += files.length;
|
|
736
742
|
for (const file of files) {
|
|
743
|
+
const baseName = file.split(/[/\\]/).pop() ?? '';
|
|
744
|
+
if (skipFiles.has(baseName))
|
|
745
|
+
continue;
|
|
737
746
|
const content = await readFile(file, 'utf8');
|
|
738
747
|
const lines = content.split('\n');
|
|
739
748
|
for (let i = 0; i < lines.length; i++) {
|
package/package.json
CHANGED
|
@@ -694,4 +694,173 @@ describe('runDoctor', () => {
|
|
|
694
694
|
expect(check?.status).toBe('PASS');
|
|
695
695
|
});
|
|
696
696
|
});
|
|
697
|
+
|
|
698
|
+
describe('checkConventionEvidence (W1)', () => {
|
|
699
|
+
async function buildConventionProject(): Promise<string> {
|
|
700
|
+
const dir = tempDir;
|
|
701
|
+
await mkdir(join(dir, 'domains', 'test-domain'), { recursive: true });
|
|
702
|
+
await mkdir(join(dir, '.mc'), { recursive: true });
|
|
703
|
+
await mkdir(join(dir, '.husky'), { recursive: true });
|
|
704
|
+
await writeFile(
|
|
705
|
+
join(dir, '.mc', 'scaffold-config.json'),
|
|
706
|
+
JSON.stringify({ domainIds: ['test-domain'] })
|
|
707
|
+
);
|
|
708
|
+
await writeFile(join(dir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
709
|
+
return dir;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
it('PASS when K-node IDs are present in domain CLAUDE.md', async () => {
|
|
713
|
+
await buildConventionProject();
|
|
714
|
+
await writeFile(
|
|
715
|
+
join(tempDir, 'domains', 'test-domain', 'CLAUDE.md'),
|
|
716
|
+
[
|
|
717
|
+
'# Test Domain',
|
|
718
|
+
'',
|
|
719
|
+
'Conventions:',
|
|
720
|
+
'- `K:exit-gate:domain-complete` — exit gate',
|
|
721
|
+
'- `K:convention:tenancy-trio` — tenancy trio',
|
|
722
|
+
'- `K:convention:no-cross-domain-import` — no cross-domain imports',
|
|
723
|
+
'- `K:convention:rls-per-domain-role` — RLS test',
|
|
724
|
+
'- `K:convention:tier-1-foundations` — tier-1 foundations',
|
|
725
|
+
'- `K:convention:flow-vs-subscriber-rule` — flow vs subscriber',
|
|
726
|
+
'',
|
|
727
|
+
].join('\n')
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
731
|
+
const check = report.checks.find(c => c.name === 'Conventions have evidence');
|
|
732
|
+
expect(check).toBeDefined();
|
|
733
|
+
expect(check?.status).toBe('PASS');
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('WARN when 1-2 K-node IDs are missing from domain files', async () => {
|
|
737
|
+
await buildConventionProject();
|
|
738
|
+
// Only add some K-node IDs
|
|
739
|
+
await writeFile(
|
|
740
|
+
join(tempDir, 'domains', 'test-domain', 'CLAUDE.md'),
|
|
741
|
+
[
|
|
742
|
+
'# Test Domain',
|
|
743
|
+
'`K:exit-gate:domain-complete`',
|
|
744
|
+
'`K:convention:tenancy-trio`',
|
|
745
|
+
'`K:convention:no-cross-domain-import`',
|
|
746
|
+
'`K:convention:rls-per-domain-role`',
|
|
747
|
+
// Missing: tier-1-foundations, flow-vs-subscriber-rule
|
|
748
|
+
].join('\n')
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
752
|
+
const check = report.checks.find(c => c.name === 'Conventions have evidence');
|
|
753
|
+
expect(check).toBeDefined();
|
|
754
|
+
expect(check?.status).toBe('WARN');
|
|
755
|
+
expect(check?.message).toMatch(/tier-1-foundations/);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('FAIL when more than 2 K-node IDs are missing', async () => {
|
|
759
|
+
await buildConventionProject();
|
|
760
|
+
await writeFile(
|
|
761
|
+
join(tempDir, 'domains', 'test-domain', 'CLAUDE.md'),
|
|
762
|
+
'# Test Domain\nNo convention references here.\n'
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
766
|
+
const check = report.checks.find(c => c.name === 'Conventions have evidence');
|
|
767
|
+
expect(check).toBeDefined();
|
|
768
|
+
expect(check?.status).toBe('FAIL');
|
|
769
|
+
expect(check?.message).toMatch(/6\/6 convention/);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('PASS when K-node IDs are found in root CLAUDE.md', async () => {
|
|
773
|
+
await buildConventionProject();
|
|
774
|
+
await writeFile(
|
|
775
|
+
join(tempDir, 'CLAUDE.md'),
|
|
776
|
+
[
|
|
777
|
+
'# Project',
|
|
778
|
+
'`K:exit-gate:domain-complete`',
|
|
779
|
+
'`K:convention:tenancy-trio`',
|
|
780
|
+
'`K:convention:no-cross-domain-import`',
|
|
781
|
+
'`K:convention:rls-per-domain-role`',
|
|
782
|
+
'`K:convention:tier-1-foundations`',
|
|
783
|
+
'`K:convention:flow-vs-subscriber-rule`',
|
|
784
|
+
].join('\n')
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
788
|
+
const check = report.checks.find(c => c.name === 'Conventions have evidence');
|
|
789
|
+
expect(check).toBeDefined();
|
|
790
|
+
expect(check?.status).toBe('PASS');
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
describe('checkAllHttpClientsUseKy skipfiles', () => {
|
|
795
|
+
async function buildBaseForCliCheck(): Promise<string> {
|
|
796
|
+
await mkdir(join(tempDir, '.mc'), { recursive: true });
|
|
797
|
+
await mkdir(join(tempDir, 'domains'), { recursive: true });
|
|
798
|
+
await mkdir(join(tempDir, '.husky'), { recursive: true });
|
|
799
|
+
await writeFile(
|
|
800
|
+
join(tempDir, '.mc', 'scaffold-config.json'),
|
|
801
|
+
JSON.stringify({ domainIds: [] })
|
|
802
|
+
);
|
|
803
|
+
await writeFile(join(tempDir, '.husky', 'pre-commit'), '#!/bin/sh');
|
|
804
|
+
return tempDir;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
it('does not flag fetch() inside generate-sdk.ts template literal', async () => {
|
|
808
|
+
await buildBaseForCliCheck();
|
|
809
|
+
const cliDir = join(tempDir, 'packages', 'domain-cli', 'src', 'commands');
|
|
810
|
+
await mkdir(cliDir, { recursive: true });
|
|
811
|
+
await writeFile(
|
|
812
|
+
join(cliDir, 'generate-sdk.ts'),
|
|
813
|
+
`export function genTemplate() {
|
|
814
|
+
return \`
|
|
815
|
+
const res = await fetch('/api/endpoint', { method: 'GET' });
|
|
816
|
+
if (!res.ok) throw new Error();
|
|
817
|
+
return res.json();
|
|
818
|
+
\`;
|
|
819
|
+
}`
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
823
|
+
const check = report.checks.find(c => c.name === 'All HTTP clients use ky');
|
|
824
|
+
expect(check?.status).toBe('PASS');
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('does not flag fetch() in doctor.ts JSDoc comments and message strings', async () => {
|
|
828
|
+
await buildBaseForCliCheck();
|
|
829
|
+
const cliDir = join(tempDir, 'packages', 'domain-cli', 'src', 'commands');
|
|
830
|
+
await mkdir(cliDir, { recursive: true });
|
|
831
|
+
await writeFile(
|
|
832
|
+
join(cliDir, 'doctor.ts'),
|
|
833
|
+
[
|
|
834
|
+
'/**',
|
|
835
|
+
' * Check W5-1: All HTTP clients use ky (no raw fetch()).',
|
|
836
|
+
' * Detects raw `fetch(` calls and FAILs.',
|
|
837
|
+
' * Allows `ctx.fetch(` and `ky.fetch(`.',
|
|
838
|
+
' */',
|
|
839
|
+
'export function check() {',
|
|
840
|
+
' return { message: "No raw fetch() calls found" };',
|
|
841
|
+
'}',
|
|
842
|
+
'',
|
|
843
|
+
].join('\n')
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
847
|
+
const check = report.checks.find(c => c.name === 'All HTTP clients use ky');
|
|
848
|
+
expect(check?.status).toBe('PASS');
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('still flags raw fetch() in non-skipped CLI files', async () => {
|
|
852
|
+
await buildBaseForCliCheck();
|
|
853
|
+
const cliDir = join(tempDir, 'packages', 'domain-cli', 'src', 'commands');
|
|
854
|
+
await mkdir(cliDir, { recursive: true });
|
|
855
|
+
await writeFile(
|
|
856
|
+
join(cliDir, 'some-command.ts'),
|
|
857
|
+
'export const cmd = () => fetch("https://api.example.com");'
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
const report = await runDoctor({ projectRoot: tempDir });
|
|
861
|
+
const check = report.checks.find(c => c.name === 'All HTTP clients use ky');
|
|
862
|
+
expect(check?.status).toBe('FAIL');
|
|
863
|
+
expect(check?.message).toMatch(/some-command\.ts/);
|
|
864
|
+
});
|
|
865
|
+
});
|
|
697
866
|
});
|
package/src/commands/doctor.ts
CHANGED
|
@@ -822,6 +822,13 @@ async function checkAllHttpClientsUseKy(projectRoot: string): Promise<DoctorChec
|
|
|
822
822
|
{ dir: join(projectRoot, 'packages', 'domain-cli', 'src', 'commands'), label: 'CLI commands' },
|
|
823
823
|
];
|
|
824
824
|
|
|
825
|
+
// Files to skip — these contain raw fetch() in comments, docs, or template
|
|
826
|
+
// literals that generate output code, not actual HTTP calls.
|
|
827
|
+
const skipFiles = new Set([
|
|
828
|
+
'generate-sdk.ts', // template literal generating SDK client code
|
|
829
|
+
'doctor.ts', // JSDoc block comments and fix-message strings self-flag
|
|
830
|
+
]);
|
|
831
|
+
|
|
825
832
|
const violations: string[] = [];
|
|
826
833
|
let totalFiles = 0;
|
|
827
834
|
|
|
@@ -831,6 +838,8 @@ async function checkAllHttpClientsUseKy(projectRoot: string): Promise<DoctorChec
|
|
|
831
838
|
totalFiles += files.length;
|
|
832
839
|
|
|
833
840
|
for (const file of files) {
|
|
841
|
+
const baseName = file.split(/[/\\]/).pop() ?? '';
|
|
842
|
+
if (skipFiles.has(baseName)) continue;
|
|
834
843
|
const content = await readFile(file, 'utf8');
|
|
835
844
|
const lines = content.split('\n');
|
|
836
845
|
for (let i = 0; i < lines.length; i++) {
|