@team-attention/hoyeon-cli 1.0.1 → 1.1.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/dist/cli.js +397 -37
- package/package.json +6 -5
- package/schemas/dev-spec-v5.schema.json +5 -0
package/dist/cli.js
CHANGED
|
@@ -402,11 +402,11 @@ var require_codegen = __commonJS({
|
|
|
402
402
|
const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
|
|
403
403
|
return `${varKind} ${this.name}${rhs};` + _n;
|
|
404
404
|
}
|
|
405
|
-
optimizeNames(names,
|
|
405
|
+
optimizeNames(names, constants2) {
|
|
406
406
|
if (!names[this.name.str])
|
|
407
407
|
return;
|
|
408
408
|
if (this.rhs)
|
|
409
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
409
|
+
this.rhs = optimizeExpr(this.rhs, names, constants2);
|
|
410
410
|
return this;
|
|
411
411
|
}
|
|
412
412
|
get names() {
|
|
@@ -423,10 +423,10 @@ var require_codegen = __commonJS({
|
|
|
423
423
|
render({ _n }) {
|
|
424
424
|
return `${this.lhs} = ${this.rhs};` + _n;
|
|
425
425
|
}
|
|
426
|
-
optimizeNames(names,
|
|
426
|
+
optimizeNames(names, constants2) {
|
|
427
427
|
if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
|
|
428
428
|
return;
|
|
429
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
429
|
+
this.rhs = optimizeExpr(this.rhs, names, constants2);
|
|
430
430
|
return this;
|
|
431
431
|
}
|
|
432
432
|
get names() {
|
|
@@ -487,8 +487,8 @@ var require_codegen = __commonJS({
|
|
|
487
487
|
optimizeNodes() {
|
|
488
488
|
return `${this.code}` ? this : void 0;
|
|
489
489
|
}
|
|
490
|
-
optimizeNames(names,
|
|
491
|
-
this.code = optimizeExpr(this.code, names,
|
|
490
|
+
optimizeNames(names, constants2) {
|
|
491
|
+
this.code = optimizeExpr(this.code, names, constants2);
|
|
492
492
|
return this;
|
|
493
493
|
}
|
|
494
494
|
get names() {
|
|
@@ -517,12 +517,12 @@ var require_codegen = __commonJS({
|
|
|
517
517
|
}
|
|
518
518
|
return nodes.length > 0 ? this : void 0;
|
|
519
519
|
}
|
|
520
|
-
optimizeNames(names,
|
|
520
|
+
optimizeNames(names, constants2) {
|
|
521
521
|
const { nodes } = this;
|
|
522
522
|
let i = nodes.length;
|
|
523
523
|
while (i--) {
|
|
524
524
|
const n = nodes[i];
|
|
525
|
-
if (n.optimizeNames(names,
|
|
525
|
+
if (n.optimizeNames(names, constants2))
|
|
526
526
|
continue;
|
|
527
527
|
subtractNames(names, n.names);
|
|
528
528
|
nodes.splice(i, 1);
|
|
@@ -575,12 +575,12 @@ var require_codegen = __commonJS({
|
|
|
575
575
|
return void 0;
|
|
576
576
|
return this;
|
|
577
577
|
}
|
|
578
|
-
optimizeNames(names,
|
|
578
|
+
optimizeNames(names, constants2) {
|
|
579
579
|
var _a;
|
|
580
|
-
this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names,
|
|
581
|
-
if (!(super.optimizeNames(names,
|
|
580
|
+
this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants2);
|
|
581
|
+
if (!(super.optimizeNames(names, constants2) || this.else))
|
|
582
582
|
return;
|
|
583
|
-
this.condition = optimizeExpr(this.condition, names,
|
|
583
|
+
this.condition = optimizeExpr(this.condition, names, constants2);
|
|
584
584
|
return this;
|
|
585
585
|
}
|
|
586
586
|
get names() {
|
|
@@ -603,10 +603,10 @@ var require_codegen = __commonJS({
|
|
|
603
603
|
render(opts) {
|
|
604
604
|
return `for(${this.iteration})` + super.render(opts);
|
|
605
605
|
}
|
|
606
|
-
optimizeNames(names,
|
|
607
|
-
if (!super.optimizeNames(names,
|
|
606
|
+
optimizeNames(names, constants2) {
|
|
607
|
+
if (!super.optimizeNames(names, constants2))
|
|
608
608
|
return;
|
|
609
|
-
this.iteration = optimizeExpr(this.iteration, names,
|
|
609
|
+
this.iteration = optimizeExpr(this.iteration, names, constants2);
|
|
610
610
|
return this;
|
|
611
611
|
}
|
|
612
612
|
get names() {
|
|
@@ -642,10 +642,10 @@ var require_codegen = __commonJS({
|
|
|
642
642
|
render(opts) {
|
|
643
643
|
return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
|
|
644
644
|
}
|
|
645
|
-
optimizeNames(names,
|
|
646
|
-
if (!super.optimizeNames(names,
|
|
645
|
+
optimizeNames(names, constants2) {
|
|
646
|
+
if (!super.optimizeNames(names, constants2))
|
|
647
647
|
return;
|
|
648
|
-
this.iterable = optimizeExpr(this.iterable, names,
|
|
648
|
+
this.iterable = optimizeExpr(this.iterable, names, constants2);
|
|
649
649
|
return this;
|
|
650
650
|
}
|
|
651
651
|
get names() {
|
|
@@ -687,11 +687,11 @@ var require_codegen = __commonJS({
|
|
|
687
687
|
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
|
|
688
688
|
return this;
|
|
689
689
|
}
|
|
690
|
-
optimizeNames(names,
|
|
690
|
+
optimizeNames(names, constants2) {
|
|
691
691
|
var _a, _b;
|
|
692
|
-
super.optimizeNames(names,
|
|
693
|
-
(_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names,
|
|
694
|
-
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names,
|
|
692
|
+
super.optimizeNames(names, constants2);
|
|
693
|
+
(_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants2);
|
|
694
|
+
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants2);
|
|
695
695
|
return this;
|
|
696
696
|
}
|
|
697
697
|
get names() {
|
|
@@ -992,7 +992,7 @@ var require_codegen = __commonJS({
|
|
|
992
992
|
function addExprNames(names, from) {
|
|
993
993
|
return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
|
|
994
994
|
}
|
|
995
|
-
function optimizeExpr(expr, names,
|
|
995
|
+
function optimizeExpr(expr, names, constants2) {
|
|
996
996
|
if (expr instanceof code_1.Name)
|
|
997
997
|
return replaceName(expr);
|
|
998
998
|
if (!canOptimize(expr))
|
|
@@ -1007,14 +1007,14 @@ var require_codegen = __commonJS({
|
|
|
1007
1007
|
return items;
|
|
1008
1008
|
}, []));
|
|
1009
1009
|
function replaceName(n) {
|
|
1010
|
-
const c =
|
|
1010
|
+
const c = constants2[n.str];
|
|
1011
1011
|
if (c === void 0 || names[n.str] !== 1)
|
|
1012
1012
|
return n;
|
|
1013
1013
|
delete names[n.str];
|
|
1014
1014
|
return c;
|
|
1015
1015
|
}
|
|
1016
1016
|
function canOptimize(e) {
|
|
1017
|
-
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 &&
|
|
1017
|
+
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants2[c.str] !== void 0);
|
|
1018
1018
|
}
|
|
1019
1019
|
}
|
|
1020
1020
|
function subtractNames(names, from) {
|
|
@@ -2976,7 +2976,7 @@ var require_compile = __commonJS({
|
|
|
2976
2976
|
const schOrFunc = root.refs[ref];
|
|
2977
2977
|
if (schOrFunc)
|
|
2978
2978
|
return schOrFunc;
|
|
2979
|
-
let _sch =
|
|
2979
|
+
let _sch = resolve5.call(this, root, ref);
|
|
2980
2980
|
if (_sch === void 0) {
|
|
2981
2981
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2982
2982
|
const { schemaId } = this.opts;
|
|
@@ -3003,7 +3003,7 @@ var require_compile = __commonJS({
|
|
|
3003
3003
|
function sameSchemaEnv(s1, s2) {
|
|
3004
3004
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3005
3005
|
}
|
|
3006
|
-
function
|
|
3006
|
+
function resolve5(root, ref) {
|
|
3007
3007
|
let sch;
|
|
3008
3008
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3009
3009
|
ref = sch;
|
|
@@ -3578,7 +3578,7 @@ var require_fast_uri = __commonJS({
|
|
|
3578
3578
|
}
|
|
3579
3579
|
return uri;
|
|
3580
3580
|
}
|
|
3581
|
-
function
|
|
3581
|
+
function resolve5(baseURI, relativeURI, options) {
|
|
3582
3582
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3583
3583
|
const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3584
3584
|
schemelessOptions.skipEscape = true;
|
|
@@ -3805,7 +3805,7 @@ var require_fast_uri = __commonJS({
|
|
|
3805
3805
|
var fastUri = {
|
|
3806
3806
|
SCHEMES,
|
|
3807
3807
|
normalize,
|
|
3808
|
-
resolve:
|
|
3808
|
+
resolve: resolve5,
|
|
3809
3809
|
resolveComponent,
|
|
3810
3810
|
equal,
|
|
3811
3811
|
serialize,
|
|
@@ -8495,6 +8495,11 @@ var dev_spec_v5_schema_default = {
|
|
|
8495
8495
|
additionalProperties: false,
|
|
8496
8496
|
properties: {
|
|
8497
8497
|
id: { type: "string" },
|
|
8498
|
+
category: {
|
|
8499
|
+
type: "string",
|
|
8500
|
+
enum: ["HP", "EP", "BC", "NI", "IT"],
|
|
8501
|
+
description: "Scenario coverage category: HP=Happy Path, EP=Edge/Error Path, BC=Boundary Condition, NI=Non-functional/Infrastructure, IT=Integration"
|
|
8502
|
+
},
|
|
8498
8503
|
given: { type: "string" },
|
|
8499
8504
|
when: { type: "string" },
|
|
8500
8505
|
then: { type: "string" },
|
|
@@ -9217,6 +9222,7 @@ Usage:
|
|
|
9217
9222
|
hoyeon-cli spec status <path> Show task completion status (exit 0=done, 1=incomplete)
|
|
9218
9223
|
hoyeon-cli spec meta <path> Show spec meta (name, goal, non_goals, mode, etc.)
|
|
9219
9224
|
hoyeon-cli spec check <path> Check internal consistency
|
|
9225
|
+
hoyeon-cli spec coverage <path> [--layer decisions|requirements|scenarios|tasks] [--json] Check spec coverage (source.ref, decision coverage, scenario min count, orphan scenarios)
|
|
9220
9226
|
hoyeon-cli spec amend --reason <feedback-id> --spec <path> Amend spec.json based on feedback
|
|
9221
9227
|
hoyeon-cli spec guide [section] Show schema guide for a section
|
|
9222
9228
|
hoyeon-cli spec scenario <scenario-id> --get <path> Get scenario details as JSON
|
|
@@ -10001,6 +10007,142 @@ async function handleMeta(args) {
|
|
|
10001
10007
|
process.stdout.write(JSON.stringify(meta, null, 2) + "\n");
|
|
10002
10008
|
process.exit(0);
|
|
10003
10009
|
}
|
|
10010
|
+
function collectScenarioSets(specData) {
|
|
10011
|
+
const allScenarioIds = /* @__PURE__ */ new Set();
|
|
10012
|
+
for (const req of specData.requirements || []) {
|
|
10013
|
+
for (const sc of req.scenarios || []) {
|
|
10014
|
+
if (sc.id) allScenarioIds.add(sc.id);
|
|
10015
|
+
}
|
|
10016
|
+
}
|
|
10017
|
+
const referencedScenarioIds = /* @__PURE__ */ new Set();
|
|
10018
|
+
for (const task of specData.tasks || []) {
|
|
10019
|
+
for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
|
|
10020
|
+
if (scenarioRef) referencedScenarioIds.add(scenarioRef);
|
|
10021
|
+
}
|
|
10022
|
+
}
|
|
10023
|
+
return { allScenarioIds, referencedScenarioIds };
|
|
10024
|
+
}
|
|
10025
|
+
var VALID_COVERAGE_LAYERS = ["decisions", "requirements", "scenarios", "tasks"];
|
|
10026
|
+
async function handleCoverage(args) {
|
|
10027
|
+
const parsed = parseArgs(args);
|
|
10028
|
+
const filePath = parsed._[0];
|
|
10029
|
+
if (!filePath) {
|
|
10030
|
+
process.stderr.write("Error: missing <path> argument\n");
|
|
10031
|
+
process.stderr.write("Usage: hoyeon-cli spec coverage <path> [--layer decisions|requirements|scenarios|tasks] [--json]\n");
|
|
10032
|
+
process.exit(1);
|
|
10033
|
+
}
|
|
10034
|
+
const layer = parsed.layer;
|
|
10035
|
+
if (layer !== void 0 && !VALID_COVERAGE_LAYERS.includes(layer)) {
|
|
10036
|
+
process.stderr.write(`Error: invalid --layer '${layer}'. Valid values: ${VALID_COVERAGE_LAYERS.join(", ")}
|
|
10037
|
+
`);
|
|
10038
|
+
process.exit(1);
|
|
10039
|
+
}
|
|
10040
|
+
const useJson = parsed.json === true;
|
|
10041
|
+
const specData = loadSpec(resolve(filePath));
|
|
10042
|
+
const gaps = [];
|
|
10043
|
+
const decisions = specData.context?.decisions || specData.decisions || [];
|
|
10044
|
+
const requirements = specData.requirements || [];
|
|
10045
|
+
const decisionIds = new Set(decisions.map((d) => d.id).filter(Boolean));
|
|
10046
|
+
const runDecisions = !layer || layer === "decisions";
|
|
10047
|
+
const runRequirements = !layer || layer === "requirements";
|
|
10048
|
+
const runScenarios = !layer || layer === "scenarios";
|
|
10049
|
+
const runTasks = !layer || layer === "tasks";
|
|
10050
|
+
if (runDecisions && decisionIds.size > 0) {
|
|
10051
|
+
for (const req of requirements) {
|
|
10052
|
+
const ref = req.source?.ref;
|
|
10053
|
+
if (ref === void 0 || ref === null) {
|
|
10054
|
+
gaps.push({
|
|
10055
|
+
layer: "decisions",
|
|
10056
|
+
check: "source.ref-integrity",
|
|
10057
|
+
message: `requirement '${req.id}' has no source.ref (decisions exist \u2014 link required)`
|
|
10058
|
+
});
|
|
10059
|
+
} else if (!decisionIds.has(ref)) {
|
|
10060
|
+
gaps.push({
|
|
10061
|
+
layer: "decisions",
|
|
10062
|
+
check: "source.ref-integrity",
|
|
10063
|
+
message: `requirement '${req.id}' source.ref '${ref}' does not match any decision ID`
|
|
10064
|
+
});
|
|
10065
|
+
}
|
|
10066
|
+
}
|
|
10067
|
+
}
|
|
10068
|
+
if (runDecisions && decisionIds.size > 0 && requirements.length > 0) {
|
|
10069
|
+
const coveredDecisionIds = /* @__PURE__ */ new Set();
|
|
10070
|
+
for (const req of requirements) {
|
|
10071
|
+
const ref = req.source?.ref;
|
|
10072
|
+
if (ref) coveredDecisionIds.add(ref);
|
|
10073
|
+
}
|
|
10074
|
+
for (const decId of decisionIds) {
|
|
10075
|
+
if (!coveredDecisionIds.has(decId)) {
|
|
10076
|
+
gaps.push({
|
|
10077
|
+
layer: "decisions",
|
|
10078
|
+
check: "decision-coverage",
|
|
10079
|
+
message: `decision '${decId}' is not referenced by any requirement source.ref`
|
|
10080
|
+
});
|
|
10081
|
+
}
|
|
10082
|
+
}
|
|
10083
|
+
}
|
|
10084
|
+
if (runRequirements) {
|
|
10085
|
+
for (const req of requirements) {
|
|
10086
|
+
const scenarios = req.scenarios || [];
|
|
10087
|
+
const anyHasCategory = scenarios.some((sc) => sc.category !== void 0);
|
|
10088
|
+
if (anyHasCategory) {
|
|
10089
|
+
const categories = new Set(scenarios.map((sc) => sc.category).filter(Boolean));
|
|
10090
|
+
const missing = [];
|
|
10091
|
+
if (!categories.has("HP")) missing.push("HP");
|
|
10092
|
+
if (!categories.has("EP")) missing.push("EP");
|
|
10093
|
+
if (!categories.has("BC")) missing.push("BC");
|
|
10094
|
+
if (missing.length > 0) {
|
|
10095
|
+
gaps.push({
|
|
10096
|
+
layer: "requirements",
|
|
10097
|
+
check: "scenario-min-count",
|
|
10098
|
+
message: `requirement '${req.id}' is missing scenario categories: ${missing.join(", ")}`
|
|
10099
|
+
});
|
|
10100
|
+
}
|
|
10101
|
+
} else {
|
|
10102
|
+
if (scenarios.length < 3) {
|
|
10103
|
+
gaps.push({
|
|
10104
|
+
layer: "requirements",
|
|
10105
|
+
check: "scenario-min-count",
|
|
10106
|
+
message: `requirement '${req.id}' has ${scenarios.length} scenario(s) but needs at least 3 (count-only mode \u2014 no category field present)`
|
|
10107
|
+
});
|
|
10108
|
+
}
|
|
10109
|
+
}
|
|
10110
|
+
}
|
|
10111
|
+
}
|
|
10112
|
+
if (runScenarios && runTasks) {
|
|
10113
|
+
const { allScenarioIds, referencedScenarioIds } = collectScenarioSets(specData);
|
|
10114
|
+
const tasksWithAC = (specData.tasks || []).filter((t) => t.acceptance_criteria?.scenarios);
|
|
10115
|
+
if (allScenarioIds.size > 0 && tasksWithAC.length > 0) {
|
|
10116
|
+
for (const scenarioId of allScenarioIds) {
|
|
10117
|
+
if (!referencedScenarioIds.has(scenarioId)) {
|
|
10118
|
+
gaps.push({
|
|
10119
|
+
layer: "scenarios",
|
|
10120
|
+
check: "orphan-scenario",
|
|
10121
|
+
message: `scenario '${scenarioId}' is defined but not referenced by any task acceptance_criteria`
|
|
10122
|
+
});
|
|
10123
|
+
}
|
|
10124
|
+
}
|
|
10125
|
+
}
|
|
10126
|
+
}
|
|
10127
|
+
if (useJson) {
|
|
10128
|
+
const result = {
|
|
10129
|
+
coverage: gaps.length === 0 ? "pass" : "fail",
|
|
10130
|
+
gaps
|
|
10131
|
+
};
|
|
10132
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
10133
|
+
process.exit(gaps.length === 0 ? 0 : 1);
|
|
10134
|
+
}
|
|
10135
|
+
if (gaps.length > 0) {
|
|
10136
|
+
process.stderr.write("Coverage gaps found:\n");
|
|
10137
|
+
for (const gap of gaps) {
|
|
10138
|
+
process.stderr.write(` [${gap.layer}/${gap.check}] ${gap.message}
|
|
10139
|
+
`);
|
|
10140
|
+
}
|
|
10141
|
+
process.exit(1);
|
|
10142
|
+
}
|
|
10143
|
+
process.stdout.write("Coverage passed: all coverage checks OK\n");
|
|
10144
|
+
process.exit(0);
|
|
10145
|
+
}
|
|
10004
10146
|
async function handleCheck(args) {
|
|
10005
10147
|
const filePath = args[0];
|
|
10006
10148
|
if (!filePath) {
|
|
@@ -10045,12 +10187,7 @@ async function handleCheck(args) {
|
|
|
10045
10187
|
}
|
|
10046
10188
|
}
|
|
10047
10189
|
}
|
|
10048
|
-
const allScenarioIds
|
|
10049
|
-
for (const req of specData.requirements || []) {
|
|
10050
|
-
for (const sc of req.scenarios || []) {
|
|
10051
|
-
if (sc.id) allScenarioIds.add(sc.id);
|
|
10052
|
-
}
|
|
10053
|
-
}
|
|
10190
|
+
const { allScenarioIds, referencedScenarioIds } = collectScenarioSets(specData);
|
|
10054
10191
|
for (const task of specData.tasks) {
|
|
10055
10192
|
for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
|
|
10056
10193
|
if (!allScenarioIds.has(scenarioRef)) {
|
|
@@ -10058,7 +10195,36 @@ async function handleCheck(args) {
|
|
|
10058
10195
|
}
|
|
10059
10196
|
}
|
|
10060
10197
|
}
|
|
10198
|
+
const decisionIds = new Set((specData.context?.decisions || specData.decisions || []).map((d) => d.id).filter(Boolean));
|
|
10199
|
+
for (const req of specData.requirements || []) {
|
|
10200
|
+
const ref = req.source?.ref;
|
|
10201
|
+
if (ref !== void 0 && ref !== null) {
|
|
10202
|
+
if (!decisionIds.has(ref)) {
|
|
10203
|
+
issues.push(`requirement '${req.id}' source.ref '${ref}' does not match any decision ID`);
|
|
10204
|
+
}
|
|
10205
|
+
}
|
|
10206
|
+
}
|
|
10207
|
+
const tasksWithAC = (specData.tasks || []).filter((t) => t.acceptance_criteria?.scenarios);
|
|
10208
|
+
if (allScenarioIds.size > 0 && tasksWithAC.length > 0) {
|
|
10209
|
+
for (const scenarioId of allScenarioIds) {
|
|
10210
|
+
if (!referencedScenarioIds.has(scenarioId)) {
|
|
10211
|
+
issues.push(`scenario '${scenarioId}' is defined but not referenced by any task acceptance_criteria`);
|
|
10212
|
+
}
|
|
10213
|
+
}
|
|
10214
|
+
}
|
|
10061
10215
|
const warnings = [];
|
|
10216
|
+
if ((specData.context?.decisions || specData.decisions || []).length > 0 && (specData.requirements || []).length > 0) {
|
|
10217
|
+
const coveredDecisionIds = /* @__PURE__ */ new Set();
|
|
10218
|
+
for (const req of specData.requirements || []) {
|
|
10219
|
+
const ref = req.source?.ref;
|
|
10220
|
+
if (ref) coveredDecisionIds.add(ref);
|
|
10221
|
+
}
|
|
10222
|
+
for (const decId of decisionIds) {
|
|
10223
|
+
if (!coveredDecisionIds.has(decId)) {
|
|
10224
|
+
warnings.push(`decision '${decId}' is not referenced by any requirement source.ref`);
|
|
10225
|
+
}
|
|
10226
|
+
}
|
|
10227
|
+
}
|
|
10062
10228
|
const fileScopeMap = /* @__PURE__ */ new Map();
|
|
10063
10229
|
for (const task of specData.tasks) {
|
|
10064
10230
|
for (const file of task.file_scope || []) {
|
|
@@ -10818,6 +10984,8 @@ async function spec(args) {
|
|
|
10818
10984
|
await handleStatus(args.slice(1));
|
|
10819
10985
|
} else if (subcommand === "meta") {
|
|
10820
10986
|
await handleMeta(args.slice(1));
|
|
10987
|
+
} else if (subcommand === "coverage") {
|
|
10988
|
+
await handleCoverage(args.slice(1));
|
|
10821
10989
|
} else if (subcommand === "check") {
|
|
10822
10990
|
await handleCheck(args.slice(1));
|
|
10823
10991
|
} else if (subcommand === "amend") {
|
|
@@ -11613,6 +11781,197 @@ async function feedback(args) {
|
|
|
11613
11781
|
}
|
|
11614
11782
|
}
|
|
11615
11783
|
|
|
11784
|
+
// src/handlers/settings-validate.js
|
|
11785
|
+
import { readFileSync as readFileSync4, existsSync as existsSync2, accessSync, constants } from "fs";
|
|
11786
|
+
import { resolve as resolve4, dirname as dirname4, join as join2 } from "path";
|
|
11787
|
+
var SETTINGS_HELP = `
|
|
11788
|
+
Usage:
|
|
11789
|
+
hoyeon-cli settings validate Validate .claude/settings.json hook configuration
|
|
11790
|
+
|
|
11791
|
+
Options:
|
|
11792
|
+
--help, -h Show this help message
|
|
11793
|
+
|
|
11794
|
+
Examples:
|
|
11795
|
+
hoyeon-cli settings validate
|
|
11796
|
+
`;
|
|
11797
|
+
var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
11798
|
+
"SessionStart",
|
|
11799
|
+
"SessionEnd",
|
|
11800
|
+
"UserPromptSubmit",
|
|
11801
|
+
"PreToolUse",
|
|
11802
|
+
"PostToolUse",
|
|
11803
|
+
"PostToolUseFailure",
|
|
11804
|
+
"Stop"
|
|
11805
|
+
]);
|
|
11806
|
+
function findSettingsJson(startDir) {
|
|
11807
|
+
let dir = startDir;
|
|
11808
|
+
while (true) {
|
|
11809
|
+
const candidate = join2(dir, ".claude", "settings.json");
|
|
11810
|
+
if (existsSync2(candidate)) {
|
|
11811
|
+
return candidate;
|
|
11812
|
+
}
|
|
11813
|
+
const parent = dirname4(dir);
|
|
11814
|
+
if (parent === dir) break;
|
|
11815
|
+
dir = parent;
|
|
11816
|
+
}
|
|
11817
|
+
return null;
|
|
11818
|
+
}
|
|
11819
|
+
function collectHookCommands(hooks) {
|
|
11820
|
+
const entries = [];
|
|
11821
|
+
for (const [eventType, matchers] of Object.entries(hooks)) {
|
|
11822
|
+
if (!Array.isArray(matchers)) continue;
|
|
11823
|
+
for (const matcherObj of matchers) {
|
|
11824
|
+
const matcher = matcherObj.matcher ?? "";
|
|
11825
|
+
const hookList = matcherObj.hooks ?? [];
|
|
11826
|
+
for (const hook of hookList) {
|
|
11827
|
+
if (hook.type === "command" && typeof hook.command === "string") {
|
|
11828
|
+
entries.push({ eventType, matcher, command: hook.command });
|
|
11829
|
+
}
|
|
11830
|
+
}
|
|
11831
|
+
}
|
|
11832
|
+
}
|
|
11833
|
+
return entries;
|
|
11834
|
+
}
|
|
11835
|
+
async function handleValidate2() {
|
|
11836
|
+
const startDir = process.cwd();
|
|
11837
|
+
const settingsPath = findSettingsJson(startDir);
|
|
11838
|
+
if (!settingsPath) {
|
|
11839
|
+
process.stderr.write("Error: .claude/settings.json not found (searched from cwd upward)\n");
|
|
11840
|
+
process.exit(1);
|
|
11841
|
+
}
|
|
11842
|
+
const projectRoot = dirname4(dirname4(settingsPath));
|
|
11843
|
+
let settings2;
|
|
11844
|
+
try {
|
|
11845
|
+
settings2 = JSON.parse(readFileSync4(settingsPath, "utf8"));
|
|
11846
|
+
} catch (err) {
|
|
11847
|
+
process.stderr.write(`Error: failed to parse settings.json: ${err.message}
|
|
11848
|
+
`);
|
|
11849
|
+
process.exit(1);
|
|
11850
|
+
}
|
|
11851
|
+
const hooks = settings2.hooks ?? {};
|
|
11852
|
+
const hookEntries = collectHookCommands(hooks);
|
|
11853
|
+
process.stdout.write(`Settings: ${settingsPath}
|
|
11854
|
+
`);
|
|
11855
|
+
process.stdout.write(`Project root: ${projectRoot}
|
|
11856
|
+
|
|
11857
|
+
`);
|
|
11858
|
+
let hasFailure = false;
|
|
11859
|
+
process.stdout.write("Check 1: Hook script path existence\n");
|
|
11860
|
+
const missingPaths = [];
|
|
11861
|
+
for (const { eventType, matcher, command } of hookEntries) {
|
|
11862
|
+
const absPath = resolve4(projectRoot, command);
|
|
11863
|
+
if (!existsSync2(absPath)) {
|
|
11864
|
+
missingPaths.push({ eventType, matcher, command, absPath });
|
|
11865
|
+
}
|
|
11866
|
+
}
|
|
11867
|
+
if (missingPaths.length === 0) {
|
|
11868
|
+
process.stdout.write(" PASS: All hook script paths exist\n");
|
|
11869
|
+
} else {
|
|
11870
|
+
hasFailure = true;
|
|
11871
|
+
process.stdout.write(` FAIL: ${missingPaths.length} missing path(s)
|
|
11872
|
+
`);
|
|
11873
|
+
for (const { eventType, command, absPath } of missingPaths) {
|
|
11874
|
+
process.stdout.write(` [${eventType}] ${command}
|
|
11875
|
+
`);
|
|
11876
|
+
process.stdout.write(` -> ${absPath} (not found)
|
|
11877
|
+
`);
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
process.stdout.write("\nCheck 2: Hook script executable bit\n");
|
|
11881
|
+
const nonExecutable = [];
|
|
11882
|
+
for (const { eventType, matcher, command } of hookEntries) {
|
|
11883
|
+
const absPath = resolve4(projectRoot, command);
|
|
11884
|
+
if (!existsSync2(absPath)) continue;
|
|
11885
|
+
try {
|
|
11886
|
+
accessSync(absPath, constants.X_OK);
|
|
11887
|
+
} catch {
|
|
11888
|
+
nonExecutable.push({ eventType, command, absPath });
|
|
11889
|
+
}
|
|
11890
|
+
}
|
|
11891
|
+
if (nonExecutable.length === 0) {
|
|
11892
|
+
process.stdout.write(" PASS: All hook scripts are executable\n");
|
|
11893
|
+
} else {
|
|
11894
|
+
hasFailure = true;
|
|
11895
|
+
process.stdout.write(` FAIL: ${nonExecutable.length} non-executable script(s)
|
|
11896
|
+
`);
|
|
11897
|
+
for (const { eventType, command } of nonExecutable) {
|
|
11898
|
+
process.stdout.write(` [${eventType}] ${command} (not executable)
|
|
11899
|
+
`);
|
|
11900
|
+
}
|
|
11901
|
+
}
|
|
11902
|
+
process.stdout.write("\nCheck 3: Valid event types\n");
|
|
11903
|
+
const invalidEventTypes = [];
|
|
11904
|
+
for (const eventType of Object.keys(hooks)) {
|
|
11905
|
+
if (!VALID_EVENT_TYPES.has(eventType)) {
|
|
11906
|
+
invalidEventTypes.push(eventType);
|
|
11907
|
+
}
|
|
11908
|
+
}
|
|
11909
|
+
if (invalidEventTypes.length === 0) {
|
|
11910
|
+
process.stdout.write(" PASS: All event types are valid\n");
|
|
11911
|
+
} else {
|
|
11912
|
+
hasFailure = true;
|
|
11913
|
+
process.stdout.write(` FAIL: ${invalidEventTypes.length} invalid event type(s)
|
|
11914
|
+
`);
|
|
11915
|
+
for (const et of invalidEventTypes) {
|
|
11916
|
+
process.stdout.write(` '${et}' is not a valid event type
|
|
11917
|
+
`);
|
|
11918
|
+
}
|
|
11919
|
+
process.stdout.write(` Valid event types: ${[...VALID_EVENT_TYPES].join(", ")}
|
|
11920
|
+
`);
|
|
11921
|
+
}
|
|
11922
|
+
process.stdout.write("\nCheck 4: Duplicate scripts in same event+matcher\n");
|
|
11923
|
+
const seen = /* @__PURE__ */ new Map();
|
|
11924
|
+
const duplicates = [];
|
|
11925
|
+
for (const { eventType, matcher, command } of hookEntries) {
|
|
11926
|
+
const key = `${eventType}::${matcher}`;
|
|
11927
|
+
if (!seen.has(key)) {
|
|
11928
|
+
seen.set(key, /* @__PURE__ */ new Set());
|
|
11929
|
+
}
|
|
11930
|
+
const commandSet = seen.get(key);
|
|
11931
|
+
if (commandSet.has(command)) {
|
|
11932
|
+
duplicates.push({ eventType, matcher, command });
|
|
11933
|
+
} else {
|
|
11934
|
+
commandSet.add(command);
|
|
11935
|
+
}
|
|
11936
|
+
}
|
|
11937
|
+
if (duplicates.length === 0) {
|
|
11938
|
+
process.stdout.write(" PASS: No duplicate scripts in same event+matcher\n");
|
|
11939
|
+
} else {
|
|
11940
|
+
hasFailure = true;
|
|
11941
|
+
process.stdout.write(` FAIL: ${duplicates.length} duplicate(s) found
|
|
11942
|
+
`);
|
|
11943
|
+
for (const { eventType, matcher, command } of duplicates) {
|
|
11944
|
+
const matcherLabel = matcher ? `matcher="${matcher}"` : 'matcher=""';
|
|
11945
|
+
process.stdout.write(` [${eventType}][${matcherLabel}] ${command}
|
|
11946
|
+
`);
|
|
11947
|
+
}
|
|
11948
|
+
}
|
|
11949
|
+
process.stdout.write("\n");
|
|
11950
|
+
if (hasFailure) {
|
|
11951
|
+
process.stdout.write("Result: FAIL\n");
|
|
11952
|
+
process.exit(1);
|
|
11953
|
+
} else {
|
|
11954
|
+
process.stdout.write("Result: PASS\n");
|
|
11955
|
+
process.exit(0);
|
|
11956
|
+
}
|
|
11957
|
+
}
|
|
11958
|
+
async function settings(args) {
|
|
11959
|
+
const subcommand = args[0];
|
|
11960
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
11961
|
+
process.stdout.write(SETTINGS_HELP);
|
|
11962
|
+
process.exit(0);
|
|
11963
|
+
}
|
|
11964
|
+
if (subcommand === "validate") {
|
|
11965
|
+
await handleValidate2();
|
|
11966
|
+
} else {
|
|
11967
|
+
process.stderr.write(`Error: unknown settings subcommand '${subcommand}'
|
|
11968
|
+
`);
|
|
11969
|
+
process.stderr.write(`Run 'hoyeon-cli settings --help' for usage.
|
|
11970
|
+
`);
|
|
11971
|
+
process.exit(1);
|
|
11972
|
+
}
|
|
11973
|
+
}
|
|
11974
|
+
|
|
11616
11975
|
// bin/dev-cli.js
|
|
11617
11976
|
var USAGE = `
|
|
11618
11977
|
hoyeon-cli \u2014 Developer workflow CLI
|
|
@@ -11639,7 +11998,8 @@ var SUBCOMMANDS = {
|
|
|
11639
11998
|
spec,
|
|
11640
11999
|
state,
|
|
11641
12000
|
session,
|
|
11642
|
-
feedback
|
|
12001
|
+
feedback,
|
|
12002
|
+
settings
|
|
11643
12003
|
};
|
|
11644
12004
|
async function main() {
|
|
11645
12005
|
const args = process.argv.slice(2);
|
|
@@ -11648,7 +12008,7 @@ async function main() {
|
|
|
11648
12008
|
process.exit(0);
|
|
11649
12009
|
}
|
|
11650
12010
|
if (args[0] === "--version") {
|
|
11651
|
-
const version = true ? "1.0
|
|
12011
|
+
const version = true ? "1.1.0" : "dev";
|
|
11652
12012
|
process.stdout.write(`hoyeon-cli v${version}
|
|
11653
12013
|
`);
|
|
11654
12014
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-attention/hoyeon-cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Developer CLI for managing dev workflow state",
|
|
6
6
|
"bin": {
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "node build.mjs"
|
|
19
|
-
},
|
|
20
17
|
"dependencies": {
|
|
21
18
|
"ajv": "^8.0.0",
|
|
22
19
|
"ajv-formats": "^3.0.0"
|
|
23
20
|
},
|
|
24
21
|
"devDependencies": {
|
|
25
22
|
"esbuild": "^0.27.3"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "node build.mjs",
|
|
26
|
+
"test": "node --test tests/*.test.mjs"
|
|
26
27
|
}
|
|
27
|
-
}
|
|
28
|
+
}
|
|
@@ -116,6 +116,11 @@
|
|
|
116
116
|
"additionalProperties": false,
|
|
117
117
|
"properties": {
|
|
118
118
|
"id": { "type": "string" },
|
|
119
|
+
"category": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"enum": ["HP", "EP", "BC", "NI", "IT"],
|
|
122
|
+
"description": "Scenario coverage category: HP=Happy Path, EP=Edge/Error Path, BC=Boundary Condition, NI=Non-functional/Infrastructure, IT=Integration"
|
|
123
|
+
},
|
|
119
124
|
"given": { "type": "string" },
|
|
120
125
|
"when": { "type": "string" },
|
|
121
126
|
"then": { "type": "string" },
|