@hone-ai/cli 1.4.0
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/bin/hone.js +2 -0
- package/hone-cli.js +4006 -0
- package/lib/README.md +119 -0
- package/lib/adversarial-negative-lint.js +149 -0
- package/lib/audit.js +156 -0
- package/lib/auto-detect.js +213 -0
- package/lib/autofix-guardrails.js +124 -0
- package/lib/branch-protection.js +256 -0
- package/lib/ci-classifier.js +150 -0
- package/lib/ci-failures.js +173 -0
- package/lib/claude-md-tokens.js +71 -0
- package/lib/compliance-check.js +62 -0
- package/lib/config-augment.js +133 -0
- package/lib/config-update.js +70 -0
- package/lib/dependency-audit.js +108 -0
- package/lib/derive-domain.js +185 -0
- package/lib/doc-registry.js +63 -0
- package/lib/doctor-admin-merge.js +185 -0
- package/lib/doctor-bind-default.js +118 -0
- package/lib/doctor-docs.js +205 -0
- package/lib/doctor-placeholders.js +144 -0
- package/lib/doctor-skill-staleness.js +122 -0
- package/lib/domain-skill-template.md +114 -0
- package/lib/editor-detect.js +169 -0
- package/lib/fast-track-ratify.js +133 -0
- package/lib/git-helpers.js +109 -0
- package/lib/hook-templates/pre-commit.sh +54 -0
- package/lib/hook-templates/pre-push.sh +72 -0
- package/lib/install-hooks.js +205 -0
- package/lib/knowledge-graph.js +188 -0
- package/lib/learnings-audit.js +254 -0
- package/lib/learnings-parse.js +331 -0
- package/lib/learnings-sync.js +75 -0
- package/lib/mcp-detect.js +154 -0
- package/lib/metrics-collect.js +214 -0
- package/lib/overlay-merge.js +267 -0
- package/lib/performance-analyzer.js +142 -0
- package/lib/pipeline-config.js +83 -0
- package/lib/pipeline-status.js +207 -0
- package/lib/pipeline-validate.js +322 -0
- package/lib/platform-detect.js +86 -0
- package/lib/platform-discover.js +334 -0
- package/lib/publish-learning.js +160 -0
- package/lib/python-install.js +84 -0
- package/lib/refresh-check.js +67 -0
- package/lib/refresh-knowledge.js +360 -0
- package/lib/rule-resolver.js +146 -0
- package/lib/security-scanner.js +168 -0
- package/lib/setup-grounding.js +138 -0
- package/lib/skill-assertions.js +276 -0
- package/lib/skill-audit-render.js +158 -0
- package/lib/skill-audit.js +391 -0
- package/lib/stack-detect.js +170 -0
- package/lib/stack-paths.js +285 -0
- package/lib/story-classifier-extract.js +203 -0
- package/lib/story-classifier.js +282 -0
- package/lib/sync-overwrite.js +47 -0
- package/lib/synthetic-pipeline.js +299 -0
- package/lib/validate-metadata.js +175 -0
- package/package.json +41 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* validate-metadata.js — SC-011 (Phase 11 of SC family).
|
|
4
|
+
* Pure helper that validates a `.github/pipeline/<STORY-ID>/metadata.yml`
|
|
5
|
+
* file against the JSON schema at
|
|
6
|
+
* `enterprise-assets/.github/schema/metadata.schema.json` (canonical) or
|
|
7
|
+
* an adopter-supplied override.
|
|
8
|
+
*
|
|
9
|
+
* Implements what SC-010 §10 documented (metadata.yml as wire protocol):
|
|
10
|
+
* 3+ agents read these files; without schema validation, typos like
|
|
11
|
+
* `fixFor` vs `fix_for` silently degrade to no-op behavior.
|
|
12
|
+
*
|
|
13
|
+
* Failure modes (all return structured result; never throw):
|
|
14
|
+
* - file missing → status: 'not-found' finding
|
|
15
|
+
* - malformed YAML → status: 'malformed-yaml' finding
|
|
16
|
+
* - schema file missing/unparseable → status: 'schema-error' finding
|
|
17
|
+
* - schema-validation errors → ERROR findings, one per ajv error
|
|
18
|
+
*
|
|
19
|
+
* Pure-helper-with-injected-IO style (Category B per cli/lib/README.md):
|
|
20
|
+
* - Reads files via fs (could be injected; kept simple for now)
|
|
21
|
+
* - Returns { findings, summary }
|
|
22
|
+
* - Never throws
|
|
23
|
+
*
|
|
24
|
+
* Issue: SC-011 — first JSON schema in the framework. Establishes the
|
|
25
|
+
* convention for any future schema (skill manifests, agent metadata,
|
|
26
|
+
* pipeline-config, etc.) per pipeline-integrity §15.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
|
|
32
|
+
const KNOWN_SEVERITIES = ['ERROR', 'WARN', 'INFO'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate a metadata.yml file against a JSON schema.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} args
|
|
38
|
+
* @param {string} args.path path to metadata.yml
|
|
39
|
+
* @param {string} args.schemaPath path to JSON schema file
|
|
40
|
+
* @returns {{
|
|
41
|
+
* findings: Array<{severity: string, code: string, path: string, message: string}>,
|
|
42
|
+
* summary: { total: number, errors: number, warnings: number, info: number },
|
|
43
|
+
* filePath: string,
|
|
44
|
+
* }}
|
|
45
|
+
*/
|
|
46
|
+
function validateMetadata({ path: filePath, schemaPath } = {}) {
|
|
47
|
+
const findings = [];
|
|
48
|
+
|
|
49
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
50
|
+
return wrap([{ severity: 'ERROR', code: 'invalid-args', path: '', message: 'path is required' }]);
|
|
51
|
+
}
|
|
52
|
+
if (!schemaPath || typeof schemaPath !== 'string') {
|
|
53
|
+
return wrap([{ severity: 'ERROR', code: 'invalid-args', path: '', message: 'schemaPath is required' }]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!fs.existsSync(filePath)) {
|
|
57
|
+
return wrap([{ severity: 'ERROR', code: 'file-not-found', path: filePath, message: `metadata file not found: ${filePath}` }]);
|
|
58
|
+
}
|
|
59
|
+
if (!fs.existsSync(schemaPath)) {
|
|
60
|
+
return wrap([{ severity: 'ERROR', code: 'schema-not-found', path: schemaPath, message: `schema file not found: ${schemaPath}` }]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse the schema
|
|
64
|
+
let schema;
|
|
65
|
+
try {
|
|
66
|
+
schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return wrap([{ severity: 'ERROR', code: 'schema-error', path: schemaPath, message: `cannot parse schema: ${e.message}` }]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse the metadata file. Use JSON_SCHEMA to keep ISO timestamps as strings
|
|
72
|
+
// (default schema auto-converts to Date objects, which the JSON Schema
|
|
73
|
+
// type=["string","null"] declarations reject).
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
const yaml = require('js-yaml');
|
|
77
|
+
parsed = yaml.load(fs.readFileSync(filePath, 'utf8'), { schema: yaml.JSON_SCHEMA });
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return wrap([{ severity: 'ERROR', code: 'malformed-yaml', path: filePath, message: `cannot parse YAML: ${e.message}` }]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Compile + validate via ajv. Use the 2020-12 entry point because the
|
|
83
|
+
// schema uses Draft 2020-12 (`$schema: .../draft/2020-12/schema`).
|
|
84
|
+
let validate;
|
|
85
|
+
try {
|
|
86
|
+
const Ajv = require('ajv/dist/2020');
|
|
87
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
88
|
+
validate = ajv.compile(schema);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return wrap([{ severity: 'ERROR', code: 'schema-error', path: schemaPath, message: `ajv compile failed: ${e.message}` }]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const valid = validate(parsed);
|
|
94
|
+
if (!valid) {
|
|
95
|
+
for (const err of validate.errors || []) {
|
|
96
|
+
findings.push({
|
|
97
|
+
severity: 'ERROR',
|
|
98
|
+
code: err.keyword || 'validation',
|
|
99
|
+
path: err.instancePath || '/',
|
|
100
|
+
message: formatAjvError(err),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return wrap(findings, filePath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatAjvError(err) {
|
|
109
|
+
const { keyword, instancePath, message, params } = err;
|
|
110
|
+
let detail = message || keyword;
|
|
111
|
+
if (params) {
|
|
112
|
+
if (params.missingProperty) detail = `missing required property '${params.missingProperty}'`;
|
|
113
|
+
else if (params.additionalProperty) detail = `unexpected property '${params.additionalProperty}'`;
|
|
114
|
+
else if (params.allowedValues) detail = `${message}: allowed [${params.allowedValues.join(', ')}]`;
|
|
115
|
+
}
|
|
116
|
+
return `${instancePath || '/'}: ${detail}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function wrap(findings, filePath) {
|
|
120
|
+
const summary = {
|
|
121
|
+
total: findings.length,
|
|
122
|
+
errors: findings.filter(f => f.severity === 'ERROR').length,
|
|
123
|
+
warnings: findings.filter(f => f.severity === 'WARN').length,
|
|
124
|
+
info: findings.filter(f => f.severity === 'INFO').length,
|
|
125
|
+
};
|
|
126
|
+
return { findings, summary, filePath: filePath || null };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validate every metadata.yml under a pipeline directory.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} args
|
|
133
|
+
* @param {string} args.repoRoot
|
|
134
|
+
* @param {string} args.schemaPath
|
|
135
|
+
* @returns {{
|
|
136
|
+
* files: Array<{ filePath, findings, summary }>,
|
|
137
|
+
* summary: { total, errors, warnings, info, fileCount, errorFileCount },
|
|
138
|
+
* }}
|
|
139
|
+
*/
|
|
140
|
+
function validateAllMetadata({ repoRoot, schemaPath }) {
|
|
141
|
+
const pipelineDir = path.join(repoRoot, '.github/pipeline');
|
|
142
|
+
if (!fs.existsSync(pipelineDir)) {
|
|
143
|
+
return {
|
|
144
|
+
files: [],
|
|
145
|
+
summary: { total: 0, errors: 0, warnings: 0, info: 0, fileCount: 0, errorFileCount: 0 },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const stories = fs.readdirSync(pipelineDir).filter(d =>
|
|
149
|
+
fs.statSync(path.join(pipelineDir, d)).isDirectory()
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const files = [];
|
|
153
|
+
for (const story of stories) {
|
|
154
|
+
const filePath = path.join(pipelineDir, story, 'metadata.yml');
|
|
155
|
+
if (!fs.existsSync(filePath)) continue;
|
|
156
|
+
files.push(validateMetadata({ path: filePath, schemaPath }));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const summary = {
|
|
160
|
+
total: files.reduce((s, f) => s + f.summary.total, 0),
|
|
161
|
+
errors: files.reduce((s, f) => s + f.summary.errors, 0),
|
|
162
|
+
warnings: files.reduce((s, f) => s + f.summary.warnings, 0),
|
|
163
|
+
info: files.reduce((s, f) => s + f.summary.info, 0),
|
|
164
|
+
fileCount: files.length,
|
|
165
|
+
errorFileCount: files.filter(f => f.summary.errors > 0).length,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return { files, summary };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
validateMetadata,
|
|
173
|
+
validateAllMetadata,
|
|
174
|
+
KNOWN_SEVERITIES,
|
|
175
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hone-ai/cli",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Hone AI — Enterprise SDLC Pipeline CLI",
|
|
5
|
+
"main": "hone-cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hone": "./bin/hone.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"hone-cli.js",
|
|
12
|
+
"lib/",
|
|
13
|
+
"!lib/*.test.js"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "echo \"No tests yet\" && exit 0",
|
|
17
|
+
"link": "npm link"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ajv": "^8.20.0",
|
|
21
|
+
"axios": "^1.6.0",
|
|
22
|
+
"commander": "^11.0.0",
|
|
23
|
+
"js-yaml": "^4.1.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"hone",
|
|
30
|
+
"ai",
|
|
31
|
+
"sdlc",
|
|
32
|
+
"pipeline",
|
|
33
|
+
"enterprise",
|
|
34
|
+
"developer-tools"
|
|
35
|
+
],
|
|
36
|
+
"author": "Hone AI",
|
|
37
|
+
"license": "UNLICENSED",
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|