@shuji-bonji/rfcxml-mcp 0.6.13 → 0.6.53
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.md +178 -50
- package/README.md +181 -51
- package/dist/cli/prefetch.d.ts +6 -1
- package/dist/cli/prefetch.d.ts.map +1 -1
- package/dist/cli/prefetch.js +53 -23
- package/dist/cli/prefetch.js.map +1 -1
- package/dist/config.d.ts +0 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -2
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +80 -7
- package/dist/constants.js.map +1 -1
- package/dist/services/checklist-generator.d.ts.map +1 -1
- package/dist/services/checklist-generator.js +72 -7
- package/dist/services/checklist-generator.js.map +1 -1
- package/dist/services/rfc-fetcher.d.ts +19 -1
- package/dist/services/rfc-fetcher.d.ts.map +1 -1
- package/dist/services/rfc-fetcher.js +116 -19
- package/dist/services/rfc-fetcher.js.map +1 -1
- package/dist/services/rfc-service.d.ts +11 -1
- package/dist/services/rfc-service.d.ts.map +1 -1
- package/dist/services/rfc-service.js +45 -5
- package/dist/services/rfc-service.js.map +1 -1
- package/dist/services/rfc-text-parser.d.ts.map +1 -1
- package/dist/services/rfc-text-parser.js +1594 -88
- package/dist/services/rfc-text-parser.js.map +1 -1
- package/dist/services/rfcxml-parser.d.ts.map +1 -1
- package/dist/services/rfcxml-parser.js +366 -98
- package/dist/services/rfcxml-parser.js.map +1 -1
- package/dist/tools/definitions.d.ts +8 -0
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +17 -1
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/handlers.d.ts +24 -12
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +161 -36
- package/dist/tools/handlers.js.map +1 -1
- package/dist/types/index.d.ts +22 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +19 -0
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +32 -0
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/disk-cache.d.ts +10 -6
- package/dist/utils/disk-cache.d.ts.map +1 -1
- package/dist/utils/disk-cache.js +13 -9
- package/dist/utils/disk-cache.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/requirement-extractor.d.ts.map +1 -1
- package/dist/utils/requirement-extractor.js +413 -21
- package/dist/utils/requirement-extractor.js.map +1 -1
- package/dist/utils/section.d.ts.map +1 -1
- package/dist/utils/section.js +11 -0
- package/dist/utils/section.js.map +1 -1
- package/dist/utils/statement-matcher.d.ts +69 -4
- package/dist/utils/statement-matcher.d.ts.map +1 -1
- package/dist/utils/statement-matcher.js +524 -48
- package/dist/utils/statement-matcher.js.map +1 -1
- package/dist/utils/text.d.ts +62 -0
- package/dist/utils/text.d.ts.map +1 -1
- package/dist/utils/text.js +273 -18
- package/dist/utils/text.js.map +1 -1
- package/package.json +5 -2
|
@@ -309,8 +309,12 @@ export function extractKeywords(text) {
|
|
|
309
309
|
* Extract requirement level from text
|
|
310
310
|
*/
|
|
311
311
|
export function extractRequirementLevel(text) {
|
|
312
|
+
// **原文どおりの大文字だけ**をレベルとみなす。`toUpperCase()` してから照合
|
|
313
|
+
// していたため、"the optional cookie extension" の optional が `OPTIONAL` に
|
|
314
|
+
// なり、RFC 8446 §4.2.2 の MUST と「レベルの対」で矛盾していた(8 件)。
|
|
315
|
+
// BCP 14 は「すべて大文字で書かれたときに限る」と定めている(RFC 8174)。
|
|
312
316
|
const regex = createRequirementRegex();
|
|
313
|
-
const match = regex.exec(text
|
|
317
|
+
const match = regex.exec(text);
|
|
314
318
|
if (match) {
|
|
315
319
|
return match[1];
|
|
316
320
|
}
|
|
@@ -419,15 +423,29 @@ export function requirementSubjectOf(requirement) {
|
|
|
419
423
|
* Score a requirement against statement keywords
|
|
420
424
|
*/
|
|
421
425
|
export function scoreRequirementMatch(requirement, statementKeywords, statementSubject, statementLevel) {
|
|
422
|
-
|
|
426
|
+
// 当たりは段落まで見る。要件文だけでは条件が拾えないことがある。
|
|
427
|
+
// ただし **どこで当たったかを分けて返す**。分けずに返していたため、
|
|
428
|
+
// `_matchedKeywords` に要件文が持たない語が並んでいた。RFC 7159 §8.1 の
|
|
429
|
+
// `Implementations MUST NOT add a byte order mark …` は、同じ段落にある
|
|
430
|
+
// 次の文の語(`ignore` `presence` `rather` `treating` `error`)まで
|
|
431
|
+
// 「一致した語」として並べていた。**その要件は ignore とは書いていない。**
|
|
432
|
+
// 実測(RFC 67 本・一致の行 1,710 件): 要件文に無い語を含む行が 1,092 件
|
|
433
|
+
// (63.9%)、最上位の一致に限ると 176 件中 28 件(15.9%)。
|
|
434
|
+
const ownText = requirement.text.toLowerCase();
|
|
435
|
+
const contextText = (requirement.fullContext || '').toLowerCase();
|
|
423
436
|
const matchedKeywords = [];
|
|
437
|
+
const contextKeywords = [];
|
|
424
438
|
let score = 0;
|
|
425
439
|
// Score based on keyword matches
|
|
426
440
|
for (const [keyword, weight] of statementKeywords) {
|
|
427
|
-
if (requirementTextHasKeyword(
|
|
441
|
+
if (requirementTextHasKeyword(ownText, keyword)) {
|
|
428
442
|
matchedKeywords.push(keyword);
|
|
429
443
|
score += weight;
|
|
430
444
|
}
|
|
445
|
+
else if (requirementTextHasKeyword(contextText, keyword)) {
|
|
446
|
+
contextKeywords.push(keyword);
|
|
447
|
+
score += weight;
|
|
448
|
+
}
|
|
431
449
|
}
|
|
432
450
|
// Bonus for subject match
|
|
433
451
|
const reqSubject = requirementSubjectOf(requirement);
|
|
@@ -444,11 +462,34 @@ export function scoreRequirementMatch(requirement, statementKeywords, statementS
|
|
|
444
462
|
requirement,
|
|
445
463
|
score,
|
|
446
464
|
matchedKeywords,
|
|
465
|
+
contextKeywords,
|
|
447
466
|
subjectMatch,
|
|
448
467
|
levelMatch,
|
|
449
468
|
};
|
|
450
469
|
}
|
|
451
|
-
|
|
470
|
+
/**
|
|
471
|
+
* どの動詞にも共通する否定の言い回し。
|
|
472
|
+
*
|
|
473
|
+
* `NEGATION_PAIRS` の `negative` は手書きだったため、動詞ごとに揃っていなかった。
|
|
474
|
+
* `mask` には "without mask" があるのに `send` には無く、RFC 2818 §2.2.1 の
|
|
475
|
+
* "Clients MUST send a closure alert before closing the connection." に対する
|
|
476
|
+
* 「The client closes the connection **without sending** a close_notify alert.」が
|
|
477
|
+
* 矛盾として挙がらなかった。
|
|
478
|
+
*
|
|
479
|
+
* 一致は部分文字列で見るので、"without send" は "without sending" にも当たる。
|
|
480
|
+
* 不規則な否定形(unmask / exclude / reject など)は各対に書く。
|
|
481
|
+
*/
|
|
482
|
+
function commonNegations(positive) {
|
|
483
|
+
return [`not ${positive}`, `never ${positive}`, `without ${positive}`];
|
|
484
|
+
}
|
|
485
|
+
/** 手書きの不規則な否定形に、共通の言い回しを足したもの。 */
|
|
486
|
+
function withCommonNegations(pairs) {
|
|
487
|
+
return pairs.map((pair) => ({
|
|
488
|
+
...pair,
|
|
489
|
+
negative: [...new Set([...pair.negative, ...commonNegations(pair.positive)])],
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
const NEGATION_PAIRS = withCommonNegations([
|
|
452
493
|
{
|
|
453
494
|
positive: 'mask',
|
|
454
495
|
negative: ['unmask', 'unmasked', 'not mask', 'without mask', 'without masking'],
|
|
@@ -489,7 +530,7 @@ const NEGATION_PAIRS = [
|
|
|
489
530
|
{ positive: 'enable', negative: ['disable', 'not enable'], generic: true },
|
|
490
531
|
{ positive: 'close', negative: ['not close', 'keep open'], generic: true },
|
|
491
532
|
{ positive: 'open', negative: ['not open', 'close'], generic: true },
|
|
492
|
-
];
|
|
533
|
+
]);
|
|
493
534
|
/**
|
|
494
535
|
* テキストが positive アクションを含むか判定(negative でないことを確認)
|
|
495
536
|
* "masks" → true, "unmasked" → false
|
|
@@ -691,6 +732,84 @@ export function identifiersOf(text) {
|
|
|
691
732
|
}
|
|
692
733
|
return [...found];
|
|
693
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* 固有の名前どうしをつなぐ語。前の名前の直後から次の名前の直前まで。
|
|
737
|
+
*
|
|
738
|
+
* "1xx (Informational) or 204 (No Content)" / "GET, PUT, or DELETE" /
|
|
739
|
+
* "Transfer-Encoding or Content-Length" / "a) the 304 … or b) the 412" の形。
|
|
740
|
+
* 句点・動詞・目的語を挟むものはつながない。
|
|
741
|
+
*/
|
|
742
|
+
const IDENTIFIER_CONNECTOR = /^\s*(?:\([^()]*\)\s*)?,?\s*(?:(?:or|and|nor|and\/or)\s+)?(?:[a-z]\)\s*)?(?:(?:a|an|the)\s+)?$/i;
|
|
743
|
+
/**
|
|
744
|
+
* 固有の名前を「すべて要る」ものと「いずれか 1 つで足りる」群に分ける。
|
|
745
|
+
*
|
|
746
|
+
* `describesSameAct` は `identifiersOf` の名前を**すべて**主張に求めていた。
|
|
747
|
+
* 要件が選言で書かれていると、片方だけを述べた違反を見逃す。RFC 9110 §8.6 の
|
|
748
|
+
* "A server MUST NOT send a Content-Length header field in any response with a
|
|
749
|
+
* status code of 1xx (Informational) or 204 (No Content)." に対し、
|
|
750
|
+
* "A server sends a Content-Length header field in a 204 response." は
|
|
751
|
+
* `1xx` が無いので「別の行為」になり、`isValid: true` を返していた。
|
|
752
|
+
* 同型の否定要件は 6 本で 11 件(RFC 9110 §9.3.6 `Transfer-Encoding or
|
|
753
|
+
* Content-Length`、RFC 9114 §4.1.1 `GET, PUT, or DELETE` など)。
|
|
754
|
+
*
|
|
755
|
+
* 名前の出現を並べ、`IDENTIFIER_CONNECTOR` でつながる連なりを 1 つの並びと
|
|
756
|
+
* みなす。並びのどこかに `or` があればその並びは選言で、いずれか 1 つが
|
|
757
|
+
* 主張にあればよい。`and` や読点だけの並び、単独の名前は従来どおりすべて要る
|
|
758
|
+
* (`MAX_PUSH_ID` のような名前は 1 つしか無いので、これまでと変わらない)。
|
|
759
|
+
*/
|
|
760
|
+
export function identifierGroupsOf(text) {
|
|
761
|
+
// 引用は落とすが、位置がずれないよう同じ長さの空白にする。
|
|
762
|
+
const scrubbed = text.replace(/\[[^\]]*\]/g, (citation) => ' '.repeat(citation.length));
|
|
763
|
+
const identifiers = identifiersOf(scrubbed);
|
|
764
|
+
const occurrences = [];
|
|
765
|
+
for (const name of identifiers) {
|
|
766
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
767
|
+
const pattern = new RegExp(`(?<![A-Za-z0-9_])${escaped}(?![A-Za-z0-9_])`, 'g');
|
|
768
|
+
for (const match of scrubbed.matchAll(pattern)) {
|
|
769
|
+
occurrences.push({ name, start: match.index, end: match.index + name.length });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
occurrences.sort((a, b) => a.start - b.start);
|
|
773
|
+
const runs = [];
|
|
774
|
+
let previous = null;
|
|
775
|
+
for (const current of occurrences) {
|
|
776
|
+
const between = previous ? scrubbed.slice(previous.end, current.start) : '';
|
|
777
|
+
const run = runs[runs.length - 1];
|
|
778
|
+
if (previous && run && IDENTIFIER_CONNECTOR.test(between)) {
|
|
779
|
+
run.names.push(current.name);
|
|
780
|
+
if (/\bor\b/i.test(between))
|
|
781
|
+
run.disjunctive = true;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
runs.push({ names: [current.name], disjunctive: false });
|
|
785
|
+
}
|
|
786
|
+
previous = current;
|
|
787
|
+
}
|
|
788
|
+
const anyOf = [];
|
|
789
|
+
const grouped = new Set();
|
|
790
|
+
for (const run of runs) {
|
|
791
|
+
if (run.disjunctive && run.names.length > 1) {
|
|
792
|
+
const names = [...new Set(run.names)];
|
|
793
|
+
anyOf.push(names);
|
|
794
|
+
for (const name of names)
|
|
795
|
+
grouped.add(name);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return { required: identifiers.filter((name) => !grouped.has(name)), anyOf };
|
|
799
|
+
}
|
|
800
|
+
/** 主張が、要件の固有の名前を(選言は 1 つで足りるとして)すべて含むか。 */
|
|
801
|
+
function statementNamesIdentifiers(statementLower, scope) {
|
|
802
|
+
const { required, anyOf } = identifierGroupsOf(scope);
|
|
803
|
+
for (const identifier of required) {
|
|
804
|
+
if (!statementLower.includes(identifier.toLowerCase()))
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
for (const group of anyOf) {
|
|
808
|
+
if (!group.some((identifier) => statementLower.includes(identifier.toLowerCase())))
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
694
813
|
/**
|
|
695
814
|
* 禁止や要求を限定している語。
|
|
696
815
|
*
|
|
@@ -727,14 +846,38 @@ export function conditionOf(text) {
|
|
|
727
846
|
const condition = clipAtClauseEnd(match[2]);
|
|
728
847
|
return condition.length > 0 ? condition : null;
|
|
729
848
|
}
|
|
730
|
-
/**
|
|
731
|
-
function
|
|
732
|
-
|
|
849
|
+
/** 内容語(機能語と BCP 14 キーワードを除いた 3 文字以上の語)。 */
|
|
850
|
+
function contentWordsOf(text) {
|
|
851
|
+
return text
|
|
733
852
|
.toLowerCase()
|
|
734
853
|
.split(/[^a-z0-9]+/)
|
|
735
854
|
.filter((word) => word.length >= MATCHING_LIMITS.MIN_KEYWORD_LENGTH && !STOP_WORDS.has(word));
|
|
736
|
-
|
|
737
|
-
|
|
855
|
+
}
|
|
856
|
+
/** 2 つの文字列に共通する内容語があるか。 */
|
|
857
|
+
function sharesContentWord(a, b) {
|
|
858
|
+
const other = contentWordsOf(b).join(' ');
|
|
859
|
+
return contentWordsOf(a).some((word) => requirementTextHasKeyword(other, word));
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* 双方の条件節に内容語があるときだけ、重なりを求める。
|
|
863
|
+
*
|
|
864
|
+
* RFC 9110 §10.1.3 の "MUST NOT include the fragment and userinfo components
|
|
865
|
+
* of the URI reference [URI], **if any**, when generating the Referer field
|
|
866
|
+
* value." は、`condition` が "any" になる。内容語が 1 つも無い条件節は
|
|
867
|
+
* 比べようがなく、求めると当の要件が常に「別の行為」になる。
|
|
868
|
+
* 片方にしか無いときと同じく、判断材料が無いので通す。
|
|
869
|
+
*/
|
|
870
|
+
function conditionsDiverge(requirementCondition, statement) {
|
|
871
|
+
if (!requirementCondition)
|
|
872
|
+
return false;
|
|
873
|
+
const statementCondition = conditionOf(statement);
|
|
874
|
+
if (!statementCondition)
|
|
875
|
+
return false;
|
|
876
|
+
if (contentWordsOf(requirementCondition).length === 0)
|
|
877
|
+
return false;
|
|
878
|
+
if (contentWordsOf(statementCondition).length === 0)
|
|
879
|
+
return false;
|
|
880
|
+
return !sharesContentWord(requirementCondition, statementCondition);
|
|
738
881
|
}
|
|
739
882
|
/**
|
|
740
883
|
* 主張の主動詞。主語語の次に来る語を採る。
|
|
@@ -752,17 +895,115 @@ export function statementMainVerb(statement, subject) {
|
|
|
752
895
|
.map((word) => word.replace(/[^a-z]/g, ''))
|
|
753
896
|
.filter(Boolean);
|
|
754
897
|
const index = words.findIndex((word) => singular(word) === subject);
|
|
755
|
-
if (index === -1
|
|
898
|
+
if (index === -1)
|
|
756
899
|
return null;
|
|
757
|
-
|
|
900
|
+
const next = afterCompoundSubject(words, index);
|
|
901
|
+
if (next >= words.length)
|
|
902
|
+
return null;
|
|
903
|
+
return words[next];
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* 複数語の主語の終わり。主語語の次の語を返す。
|
|
907
|
+
*
|
|
908
|
+
* `SUBJECT_TERMS` は 1 語ずつ持つので、"user agent" の主語は `user` になる。
|
|
909
|
+
* その次の語 `agent` を動詞の位置として読むと、`findStatementVerb` は
|
|
910
|
+
* 限定語でない語に当たって `null` を返し、**矛盾検出が一切動かない**。
|
|
911
|
+
* RFC 9110 §10.1.3 の Referer の禁止に反する主張が `isValid: true` になっていた
|
|
912
|
+
* (RFC 9110 で 36 件、RFC 6265 で 62 件中 38 件の要件が主語 `user agent`)。
|
|
913
|
+
*
|
|
914
|
+
* 主語語が続いているあいだ("user agent" "proxy server")は主語の一部として
|
|
915
|
+
* 読み飛ばす。"a user sends …" は次の語が主語語ではないので従来どおり。
|
|
916
|
+
*/
|
|
917
|
+
function afterCompoundSubject(words, subjectIndex) {
|
|
918
|
+
let index = subjectIndex + 1;
|
|
919
|
+
while (index < words.length && SUBJECT_TERMS.has(singular(words[index])))
|
|
920
|
+
index++;
|
|
921
|
+
return index;
|
|
758
922
|
}
|
|
759
923
|
/** 主張が実際にその動詞の行為を述べているか(主動詞で見る)。 */
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
924
|
+
/** 主語と動詞の間に挟まる限定句を、何語まで飛ばすか。 */
|
|
925
|
+
const SUBJECT_TO_VERB_GAP = 8;
|
|
926
|
+
/**
|
|
927
|
+
* 主張の中でその行為を述べている動詞の位置を返す。無ければ `null`。
|
|
928
|
+
*
|
|
929
|
+
* 主語の**直後**だけを見ると、限定句を挟む主張で動詞が見つからない。
|
|
930
|
+
*
|
|
931
|
+
* - `An origin server without a clock generates a Date header field.`
|
|
932
|
+
* → 主語 "server" の次は "without" で、動詞ではない
|
|
933
|
+
*
|
|
934
|
+
* 主語から数語のあいだを見て、その行為の動詞(同義語を含む)を探す。
|
|
935
|
+
*/
|
|
936
|
+
function findStatementVerb(statement, subject, verb) {
|
|
937
|
+
if (!subject)
|
|
938
|
+
return null;
|
|
939
|
+
const words = statement
|
|
940
|
+
.toLowerCase()
|
|
941
|
+
.split(/\s+/)
|
|
942
|
+
.map((word) => word.replace(/[^a-z']/g, ''))
|
|
943
|
+
.filter(Boolean);
|
|
944
|
+
const found = words.findIndex((word) => singular(word) === subject);
|
|
945
|
+
if (found === -1)
|
|
946
|
+
return null;
|
|
947
|
+
// 複数語の主語("user agent")は、最後の主語語のあとから動詞を探す。
|
|
948
|
+
const start = afterCompoundSubject(words, found) - 1;
|
|
764
949
|
const group = VERB_SYNONYMS.find((synonyms) => synonyms.includes(verb)) ?? [verb];
|
|
765
|
-
|
|
950
|
+
const limit = Math.min(words.length, start + 1 + SUBJECT_TO_VERB_GAP);
|
|
951
|
+
// 飛ばしてよいのは、主語のすぐあとに続く限定句だけである。
|
|
952
|
+
// 語なら何でも飛ばすと、目的語の中の語を動詞と取り違える。
|
|
953
|
+
//
|
|
954
|
+
// "The server removes masking for data frames received from a client."
|
|
955
|
+
//
|
|
956
|
+
// ここで "masking" を動詞と取ると、「サーバはマスクしてはならない」に
|
|
957
|
+
// 違反していると誤って報告する(実際はマスクを外す側の話である)。
|
|
958
|
+
let previousWasQualifier = false;
|
|
959
|
+
for (let index = start + 1; index < limit; index++) {
|
|
960
|
+
const word = words[index];
|
|
961
|
+
if (group.some((synonym) => word.startsWith(synonym))) {
|
|
962
|
+
return { index, words, matched: word };
|
|
963
|
+
}
|
|
964
|
+
const isQualifier = SUBJECT_QUALIFIER_WORDS.has(word);
|
|
965
|
+
if (!isQualifier && !previousWasQualifier)
|
|
966
|
+
return null;
|
|
967
|
+
previousWasQualifier = isQualifier;
|
|
968
|
+
}
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* 主語と動詞の間に挟まってよい語。
|
|
973
|
+
*
|
|
974
|
+
* 「限定句のすぐ次の 1 語」も飛ばす("without **a clock** generates" の "clock")。
|
|
975
|
+
*/
|
|
976
|
+
const SUBJECT_QUALIFIER_WORDS = new Set([
|
|
977
|
+
'with',
|
|
978
|
+
'without',
|
|
979
|
+
// `findQualifierOnlyViolation` の `saysTheSame` が拾う言い換え。ここに無いと
|
|
980
|
+
// "An origin server lacking a clock generates …" で動詞に届かず、
|
|
981
|
+
// `isValid: true` になっていた("that lacks a clock" は `that` で届く)。
|
|
982
|
+
'lacking',
|
|
983
|
+
'lacks',
|
|
984
|
+
'lack',
|
|
985
|
+
'missing',
|
|
986
|
+
'that',
|
|
987
|
+
'which',
|
|
988
|
+
'who',
|
|
989
|
+
'has',
|
|
990
|
+
'have',
|
|
991
|
+
'having',
|
|
992
|
+
'no',
|
|
993
|
+
'a',
|
|
994
|
+
'an',
|
|
995
|
+
'the',
|
|
996
|
+
'of',
|
|
997
|
+
'in',
|
|
998
|
+
'on',
|
|
999
|
+
'for',
|
|
1000
|
+
'its',
|
|
1001
|
+
'their',
|
|
1002
|
+
'and',
|
|
1003
|
+
'or',
|
|
1004
|
+
]);
|
|
1005
|
+
export function statementPerformsVerb(statement, subject, verb) {
|
|
1006
|
+
return findStatementVerb(statement, subject, verb) !== null;
|
|
766
1007
|
}
|
|
767
1008
|
/**
|
|
768
1009
|
* 主張と要求アクションが同じ事柄についてのものか。
|
|
@@ -783,25 +1024,23 @@ export function statementPerformsVerb(statement, subject, verb) {
|
|
|
783
1024
|
* 3. 双方に条件節があるなら、内容語が 1 語以上重なること
|
|
784
1025
|
* (片方にしか無い場合は判断材料が無いので通す)
|
|
785
1026
|
*/
|
|
786
|
-
export function describesSameAct(statement, requirement, action) {
|
|
1027
|
+
export function describesSameAct(statement, requirement, action, options = {}) {
|
|
787
1028
|
const lower = statement.toLowerCase();
|
|
788
1029
|
// 名前と限定は要件文全体から取る。要求アクションの外に置かれることがある。
|
|
789
1030
|
// "An origin server **without a clock** MUST NOT generate a Date header field."
|
|
790
1031
|
// "The **HEAD** method is identical to GET except that the server MUST NOT send content …"
|
|
791
1032
|
const scope = `${requirement.text} ${action}`;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const statementCondition = conditionOf(statement);
|
|
801
|
-
if (requirement.condition && statementCondition) {
|
|
802
|
-
if (!sharesContentWord(requirement.condition, statementCondition))
|
|
803
|
-
return false;
|
|
1033
|
+
// 選言("1xx … or 204")の名前は、いずれか 1 つが主張にあればよい。
|
|
1034
|
+
if (!statementNamesIdentifiers(lower, scope))
|
|
1035
|
+
return false;
|
|
1036
|
+
if (!options.ignoreQualifiers) {
|
|
1037
|
+
for (const qualifier of qualifiersOf(scope)) {
|
|
1038
|
+
if (!lower.includes(qualifier))
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
804
1041
|
}
|
|
1042
|
+
if (conditionsDiverge(requirement.condition, statement))
|
|
1043
|
+
return false;
|
|
805
1044
|
return true;
|
|
806
1045
|
}
|
|
807
1046
|
/**
|
|
@@ -845,8 +1084,28 @@ const VERB_SYNONYMS = [
|
|
|
845
1084
|
* 否定を含む主張は、禁止に従っていることを述べている見込みが高いので、
|
|
846
1085
|
* 「禁止された行為をしている」検査の対象から外す。
|
|
847
1086
|
*/
|
|
848
|
-
|
|
849
|
-
|
|
1087
|
+
/** 動詞に否定が付いているか。直前の 2 語と、直後の 2 語を見る。 */
|
|
1088
|
+
const NEGATION_WORD = /^(?:not|never|cannot|without|refrain|neither|nor|no)$|n't$/;
|
|
1089
|
+
/**
|
|
1090
|
+
* 動詞の後ろに置かれる否定。"sends **no** content" / "sends **nothing**" /
|
|
1091
|
+
* "masks **none** of the frames"。
|
|
1092
|
+
*
|
|
1093
|
+
* 動詞の前しか見ていなかったため、RFC 9110 §9.3.8 の "A client MUST NOT send
|
|
1094
|
+
* content in a TRACE request." に対する "The client sends no content in a TRACE
|
|
1095
|
+
* request." が「禁じられた行為をしている」になり、`isValid: false` を返していた。
|
|
1096
|
+
* 準拠を述べている文である。
|
|
1097
|
+
*/
|
|
1098
|
+
const NEGATION_AFTER_VERB = /^(?:no|nothing|none|neither)$/;
|
|
1099
|
+
function isNegatedVerb(words, verbIndex) {
|
|
1100
|
+
for (let offset = 1; offset <= 2; offset++) {
|
|
1101
|
+
const before = words[verbIndex - offset];
|
|
1102
|
+
if (before && NEGATION_WORD.test(before))
|
|
1103
|
+
return true;
|
|
1104
|
+
const after = words[verbIndex + offset];
|
|
1105
|
+
if (after && NEGATION_AFTER_VERB.test(after))
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
return false;
|
|
850
1109
|
}
|
|
851
1110
|
/**
|
|
852
1111
|
* 禁じられた行為の主動詞を取り出す。
|
|
@@ -893,21 +1152,37 @@ function statementHasTerm(statement, term) {
|
|
|
893
1152
|
*
|
|
894
1153
|
* 主語の一致は呼び出し側(`detectConflicts`)が先に確かめている。
|
|
895
1154
|
*/
|
|
896
|
-
export function findProhibitionViolation(statement, forbiddenAction, subject) {
|
|
1155
|
+
export function findProhibitionViolation(statement, forbiddenAction, subject, requirementSubject) {
|
|
897
1156
|
const statementLower = statement.toLowerCase();
|
|
898
|
-
if (hasNegation(statementLower))
|
|
899
|
-
return null;
|
|
900
1157
|
const verb = headVerbOf(forbiddenAction);
|
|
901
1158
|
if (!verb)
|
|
902
1159
|
return null;
|
|
903
1160
|
// 主張の主動詞がその行為であること。文中のどこかに動詞が現れるだけでは、
|
|
904
1161
|
// 別の行為を述べている見込みがある。
|
|
905
|
-
|
|
1162
|
+
const found = findStatementVerb(statementLower, subject, verb);
|
|
1163
|
+
if (!found)
|
|
906
1164
|
return null;
|
|
907
|
-
|
|
1165
|
+
// 否定は**動詞に付いたもの**だけを見る。文全体を見ると、要件の条件を
|
|
1166
|
+
// そのまま書き写した主張が落ちる。
|
|
1167
|
+
//
|
|
1168
|
+
// "An origin server without a clock generates a Date header field."
|
|
1169
|
+
//
|
|
1170
|
+
// この "without" は行為を否定していない。要件
|
|
1171
|
+
// "An origin server without a clock MUST NOT generate a Date header field."
|
|
1172
|
+
// の条件を書き写しただけで、これは違反そのものである。
|
|
1173
|
+
if (isNegatedVerb(found.words, found.index))
|
|
1174
|
+
return null;
|
|
1175
|
+
const matchedVerb = found.matched;
|
|
908
1176
|
const sharedTerms = [];
|
|
909
1177
|
const seen = new Set();
|
|
910
|
-
|
|
1178
|
+
// 行為が短い禁止は、動詞を除くと共通語が 3 個に届かず落ちていた。
|
|
1179
|
+
//
|
|
1180
|
+
// "clients MUST NOT show it to end users."(RFC 6455 §5.5.1)
|
|
1181
|
+
// → 行為 "show it to end users" の内容語は "end" と "users" の 2 個
|
|
1182
|
+
//
|
|
1183
|
+
// 何を誰に禁じているかは要件の主語にもあるので、そちらも語の元にする。
|
|
1184
|
+
const pool = `${forbiddenAction} ${requirementSubject ?? ''}`;
|
|
1185
|
+
for (const word of pool.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
911
1186
|
if (!word || word === verb)
|
|
912
1187
|
continue;
|
|
913
1188
|
if (word.length < MATCHING_LIMITS.MIN_KEYWORD_LENGTH)
|
|
@@ -924,10 +1199,201 @@ export function findProhibitionViolation(statement, forbiddenAction, subject) {
|
|
|
924
1199
|
return null;
|
|
925
1200
|
return { verb: matchedVerb, sharedTerms };
|
|
926
1201
|
}
|
|
1202
|
+
/**
|
|
1203
|
+
* 判断を取り下げるかを決めるための「同じ行為か」。
|
|
1204
|
+
*
|
|
1205
|
+
* `describesSameAct` の 3 条件のうち、**限定語の一致だけは求めない**。
|
|
1206
|
+
* 矛盾を主張するときは、要件に `unless` `except` `without` があって主張に
|
|
1207
|
+
* 無ければ落とすのが正しい。ここは逆で、限定語に触れていない主張こそ
|
|
1208
|
+
* 判断できない。RFC 6455 §5.4 の
|
|
1209
|
+
* "The fragments of one message MUST NOT be interleaved … unless an extension
|
|
1210
|
+
* has been negotiated …" に対し、`unless` を求めると当の要件が落ち、
|
|
1211
|
+
* 順位の下の無関係な要件("MUST NOT be fragmented")を名指ししていた。
|
|
1212
|
+
*
|
|
1213
|
+
* 固有の名前(`identifiersOf`)と条件節の重なりは `describesSameAct` と同じく求める。
|
|
1214
|
+
*/
|
|
1215
|
+
function sameActForWithdrawal(statementLower, requirement, action) {
|
|
1216
|
+
const scope = `${requirement.text} ${action}`;
|
|
1217
|
+
if (!statementNamesIdentifiers(statementLower, scope))
|
|
1218
|
+
return false;
|
|
1219
|
+
if (conditionsDiverge(requirement.condition, statementLower))
|
|
1220
|
+
return false;
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* 限定語の言い換えだけで矛盾を落とした一致を返す。無ければ `null`。
|
|
1225
|
+
*
|
|
1226
|
+
* `describesSameAct` は、要件に限定語(`without` `except` `unless` など)が
|
|
1227
|
+
* あれば主張にも同じ語を求める。要件を互いに区別しているのがその語だからである
|
|
1228
|
+
* (RFC 9110 §6.6.1 は "An origin server **with** a clock MUST generate a Date
|
|
1229
|
+
* header field" と "An origin server **without** a clock MUST NOT generate a
|
|
1230
|
+
* Date header field" を並べて書く)。
|
|
1231
|
+
*
|
|
1232
|
+
* 言い換えられると当たらない。"The origin server generates a Date header field
|
|
1233
|
+
* even though it **has no clock**." は `without` を含まないので、
|
|
1234
|
+
* `MUST NOT` に反しているのに矛盾が出ず、`isValid` が `true` になっていた。
|
|
1235
|
+
* **違反している主張に「矛盾なし」と答えることになる。**
|
|
1236
|
+
*
|
|
1237
|
+
* 限定語を無視すれば矛盾が出るときだけ拾い、`isValid` を `null` にする。
|
|
1238
|
+
* 「違反している」とは言わない — 言い換えなのか別の話なのかは決められない。
|
|
1239
|
+
*/
|
|
1240
|
+
export function findQualifierOnlyViolation(statement, matches) {
|
|
1241
|
+
const lower = statement.toLowerCase();
|
|
1242
|
+
for (const match of matches) {
|
|
1243
|
+
const req = match.requirement;
|
|
1244
|
+
if (!NEGATIVE_LEVELS.has(req.level))
|
|
1245
|
+
continue;
|
|
1246
|
+
// `without X` だけを見る。ほかの限定語(`unless` `except` `arbitrarily`)は
|
|
1247
|
+
// 言い換えの形が定まらず、言い換えなのか別の話なのかを見分けられない。
|
|
1248
|
+
const scope = `${req.text} ${requiredActionOf(req) ?? ''}`;
|
|
1249
|
+
const withoutWhat = /\bwithout\s+(?:a|an|the)?\s*([a-z][\w-]*)/i.exec(scope);
|
|
1250
|
+
if (!withoutWhat)
|
|
1251
|
+
continue;
|
|
1252
|
+
if (lower.includes('without'))
|
|
1253
|
+
continue;
|
|
1254
|
+
// 主張が同じ否定を述べていること。"with a clock"(逆の枝)は取り下げない。
|
|
1255
|
+
const noun = withoutWhat[1].toLowerCase();
|
|
1256
|
+
const saysTheSame = new RegExp(`\\b(?:no|not\\s+have|does\\s+not\\s+have|lacks?|lacking|missing)\\b[^.]{0,30}\\b${noun}s?\\b`, 'i').test(lower);
|
|
1257
|
+
if (!saysTheSame)
|
|
1258
|
+
continue;
|
|
1259
|
+
if (detectConflicts(statement, [req]).length > 0)
|
|
1260
|
+
continue;
|
|
1261
|
+
if (detectConflicts(statement, [req], { ignoreQualifiers: true }).length === 0)
|
|
1262
|
+
continue;
|
|
1263
|
+
return { requirement: req, qualifier: 'without' };
|
|
1264
|
+
}
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
/** 受動態の行為。`MUST NOT` の直後が "be 過去分詞" になっている。 */
|
|
1268
|
+
const PASSIVE_ACTION_PATTERN = /^be\s+[a-z]+(?:ed|n|t)\b/;
|
|
1269
|
+
/**
|
|
1270
|
+
* 受動態で書かれた禁止のうち、違反しているかを機械で決められないものを返す。
|
|
1271
|
+
*
|
|
1272
|
+
* 矛盾検出は、文の主語が禁じられた動詞を実行しているかを見る
|
|
1273
|
+
* (`statementPerformsVerb`)。要件が "A reference identity of type CN-ID
|
|
1274
|
+
* MUST NOT be used by clients." のように受動態で書かれていると、禁じられた
|
|
1275
|
+
* 行為に実行者がいない。文が "The client uses the cn-id identifier type."
|
|
1276
|
+
* でも矛盾は出ず、`conflicts` は空になる。
|
|
1277
|
+
*
|
|
1278
|
+
* 空の `conflicts` をそのまま `isValid: true` にすると、違反している文に
|
|
1279
|
+
* 「矛盾なし」と返す。ここで拾って `null`(判断できない)にする。
|
|
1280
|
+
*
|
|
1281
|
+
* 実測(RFC 9110/9111/9112/9113/9114/6455/8446/5280 の `MUST NOT` から機械で
|
|
1282
|
+
* 作った受動態の違反文 40 件): `isValid` は `true` が 13 件、`null` が 27 件、
|
|
1283
|
+
* `false` は 0 件だった。この 13 件が誤解を招く形である。
|
|
1284
|
+
*/
|
|
1285
|
+
export function findUndecidablePassiveProhibition(statement, matches) {
|
|
1286
|
+
const lower = statement.toLowerCase();
|
|
1287
|
+
// 文自身が否定なら、準拠していると述べている。判断を取り下げる必要はない。
|
|
1288
|
+
if (/\b(?:not|never|no|cannot)\b/.test(lower))
|
|
1289
|
+
return null;
|
|
1290
|
+
for (const match of matches) {
|
|
1291
|
+
const req = match.requirement;
|
|
1292
|
+
if (!NEGATIVE_LEVELS.has(req.level))
|
|
1293
|
+
continue;
|
|
1294
|
+
const action = (requiredActionOf(req) ?? '')
|
|
1295
|
+
.replace(/must not|shall not|should not|not recommended/gi, '')
|
|
1296
|
+
.trim()
|
|
1297
|
+
.toLowerCase();
|
|
1298
|
+
if (!PASSIVE_ACTION_PATTERN.test(action))
|
|
1299
|
+
continue;
|
|
1300
|
+
if (!sameActForWithdrawal(lower, req, action))
|
|
1301
|
+
continue;
|
|
1302
|
+
return req;
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* 主張と要件が、同じ動詞について述べているか。
|
|
1308
|
+
*
|
|
1309
|
+
* レベルの対(`MAY` と `MUST NOT` など)だけで矛盾を出していた。重なりの条件は
|
|
1310
|
+
* 文全体のキーワードを数えるので、主語と共通の名詞で埋まる。**動詞が違っても
|
|
1311
|
+
* 当たる。**
|
|
1312
|
+
*
|
|
1313
|
+
* RFC 7159 §8.1 の
|
|
1314
|
+
* `implementations that parse JSON texts MAY **ignore** the presence of a byte
|
|
1315
|
+
* order mark` を主張として渡すと、`Implementations MUST NOT **add** a byte
|
|
1316
|
+
* order mark to the beginning of a JSON text.` と `byte order mark` で重なり、
|
|
1317
|
+
* 「MAY と MUST NOT」で矛盾になっていた。無視することと足すことは別の行為である。
|
|
1318
|
+
* **RFC 自身の文をそのまま渡して「矛盾あり」と答えることになる。**
|
|
1319
|
+
*
|
|
1320
|
+
* 実測(RFC 67 本から要件文 300 件をそのまま主張として渡す):
|
|
1321
|
+
* `isValid: false` が 34 件 → 10 件。作った違反文 157 件の検出は 59 → 58 件。
|
|
1322
|
+
*
|
|
1323
|
+
* 目的語まで一致を求める案(`sharesContentWord` と `describesSameAct` を足す)も
|
|
1324
|
+
* 測った。誤検出は 10 → 7 件に減るが、検出が 58 → 53 件に落ちる。3 件のために
|
|
1325
|
+
* 5 件を落とすので採らない。
|
|
1326
|
+
*/
|
|
1327
|
+
function sameActVerb(statement, requirement) {
|
|
1328
|
+
const statementAct = actAfterKeyword(statement);
|
|
1329
|
+
const requirementAct = requiredActionOf(requirement) ?? actAfterKeyword(requirement.text);
|
|
1330
|
+
if (!statementAct || !requirementAct)
|
|
1331
|
+
return true;
|
|
1332
|
+
const a = actVerb(statementAct);
|
|
1333
|
+
const b = actVerb(requirementAct);
|
|
1334
|
+
if (!a || !b || a === b)
|
|
1335
|
+
return true;
|
|
1336
|
+
return VERB_SYNONYMS.some((group) => {
|
|
1337
|
+
const stems = group.map(stemVerb);
|
|
1338
|
+
return stems.includes(a) && stems.includes(b);
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
/** BCP 14 のキーワードより後ろ。行為にあたる部分。 */
|
|
1342
|
+
function actAfterKeyword(text) {
|
|
1343
|
+
const match = /\b(?:MUST NOT|SHALL NOT|SHOULD NOT|NOT RECOMMENDED|MUST|SHALL|SHOULD|MAY|REQUIRED|RECOMMENDED|OPTIONAL)\b/.exec(text);
|
|
1344
|
+
if (!match)
|
|
1345
|
+
return null;
|
|
1346
|
+
const act = text.slice(match.index + match[0].length).trim();
|
|
1347
|
+
return act.length > 0 ? act : null;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* 行為の主動詞。副詞・助動詞・指示語を飛ばして最初の語を採る。
|
|
1351
|
+
*
|
|
1352
|
+
* 受動態(`be set as a status code`)では `be` を飛ばして `set` を採る。
|
|
1353
|
+
*/
|
|
1354
|
+
const ACT_LEAD_WORDS = new Set([
|
|
1355
|
+
'be',
|
|
1356
|
+
'been',
|
|
1357
|
+
'being',
|
|
1358
|
+
'not',
|
|
1359
|
+
'only',
|
|
1360
|
+
'also',
|
|
1361
|
+
'always',
|
|
1362
|
+
'never',
|
|
1363
|
+
'immediately',
|
|
1364
|
+
'then',
|
|
1365
|
+
'either',
|
|
1366
|
+
'both',
|
|
1367
|
+
'still',
|
|
1368
|
+
'otherwise',
|
|
1369
|
+
'therefore',
|
|
1370
|
+
'thus',
|
|
1371
|
+
'further',
|
|
1372
|
+
'again',
|
|
1373
|
+
'that',
|
|
1374
|
+
'this',
|
|
1375
|
+
'it',
|
|
1376
|
+
'they',
|
|
1377
|
+
'to',
|
|
1378
|
+
]);
|
|
1379
|
+
/** 語尾を落として比べる。`sending` と `sends` と `send` を同じにする。 */
|
|
1380
|
+
function stemVerb(word) {
|
|
1381
|
+
return word.replace(/(?:ing|ed|es|s)$/, '');
|
|
1382
|
+
}
|
|
1383
|
+
function actVerb(act) {
|
|
1384
|
+
for (const raw of act.toLowerCase().split(/[^a-z]+/)) {
|
|
1385
|
+
if (!raw)
|
|
1386
|
+
continue;
|
|
1387
|
+
if (ACT_LEAD_WORDS.has(raw))
|
|
1388
|
+
continue;
|
|
1389
|
+
return stemVerb(raw);
|
|
1390
|
+
}
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
927
1393
|
/**
|
|
928
1394
|
* Detect conflicts between statement and requirements
|
|
929
1395
|
*/
|
|
930
|
-
export function detectConflicts(statement, requirements) {
|
|
1396
|
+
export function detectConflicts(statement, requirements, options = {}) {
|
|
931
1397
|
const conflicts = [];
|
|
932
1398
|
const statementLevel = extractRequirementLevel(statement);
|
|
933
1399
|
const statementSubject = extractSubject(statement);
|
|
@@ -966,8 +1432,9 @@ export function detectConflicts(statement, requirements) {
|
|
|
966
1432
|
if (requirementTextHasKeyword(reqText, keyword))
|
|
967
1433
|
overlap++;
|
|
968
1434
|
}
|
|
969
|
-
|
|
970
|
-
statementKeywords.size <= MATCHING_LIMITS.SHORT_STATEMENT_THRESHOLD
|
|
1435
|
+
const enough = overlap >= MATCHING_LIMITS.MIN_OVERLAP_FOR_CONFLICT ||
|
|
1436
|
+
statementKeywords.size <= MATCHING_LIMITS.SHORT_STATEMENT_THRESHOLD;
|
|
1437
|
+
if (enough && sameActVerb(statement, req)) {
|
|
971
1438
|
conflicts.push({
|
|
972
1439
|
requirement: req,
|
|
973
1440
|
reason: `Statement uses "${statementLevel}" but requirement uses "${req.level}"`,
|
|
@@ -986,7 +1453,7 @@ export function detectConflicts(statement, requirements) {
|
|
|
986
1453
|
const reqLevel = req.level;
|
|
987
1454
|
// For MUST requirements: check if statement contradicts the required action
|
|
988
1455
|
if (reqAction && (reqLevel === 'MUST' || reqLevel === 'SHALL' || reqLevel === 'REQUIRED')) {
|
|
989
|
-
const contradiction = describesSameAct(statementLower, req, reqAction)
|
|
1456
|
+
const contradiction = describesSameAct(statementLower, req, reqAction, options)
|
|
990
1457
|
? findActionContradiction(statementLower, reqAction, statementKeywords)
|
|
991
1458
|
: null;
|
|
992
1459
|
if (contradiction) {
|
|
@@ -1013,13 +1480,17 @@ export function detectConflicts(statement, requirements) {
|
|
|
1013
1480
|
// 禁じられているのはこの動詞であること(付随的な言及ではない)
|
|
1014
1481
|
if (!forbiddenHeadVerb || !forbiddenHeadVerb.startsWith(pair.positive))
|
|
1015
1482
|
continue;
|
|
1016
|
-
|
|
1017
|
-
|
|
1483
|
+
// 動詞の後ろの否定("sends no content")は `hasPositiveAction` の
|
|
1484
|
+
// 否定形一覧("not send")に無い。動詞の位置で否定を見る。
|
|
1485
|
+
const performed = hasPositiveAction(statementLower, pair)
|
|
1486
|
+
? findStatementVerb(statementLower, statementSubject, pair.positive)
|
|
1487
|
+
: null;
|
|
1488
|
+
const statementDoesPositive = performed !== null && !isNegatedVerb(performed.words, performed.index);
|
|
1018
1489
|
// 一般的な動詞では、動詞が一致しただけの当たりを落とす(例: "MUST NOT send
|
|
1019
1490
|
// back a |Sec-WebSocket-Protocol| header field" と「マスクなしのフレームを送る」)
|
|
1020
1491
|
if (statementDoesPositive &&
|
|
1021
1492
|
sharesActionContext(statementKeywords, forbiddenAction, pair) &&
|
|
1022
|
-
describesSameAct(statementLower, req, forbiddenAction)) {
|
|
1493
|
+
describesSameAct(statementLower, req, forbiddenAction, options)) {
|
|
1023
1494
|
conflicts.push({
|
|
1024
1495
|
requirement: req,
|
|
1025
1496
|
reason: `Statement does what "${reqLevel}" forbids: "${forbiddenAction}"`,
|
|
@@ -1034,8 +1505,8 @@ export function detectConflicts(statement, requirements) {
|
|
|
1034
1505
|
// `NEGATION_PAIRS` は肯定形と否定形の対しか見ないため、ここで漏れていた。
|
|
1035
1506
|
if (!pairConflict &&
|
|
1036
1507
|
forbiddenAction &&
|
|
1037
|
-
describesSameAct(statementLower, req, forbiddenAction)) {
|
|
1038
|
-
const violation = findProhibitionViolation(statementLower, forbiddenAction, statementSubject);
|
|
1508
|
+
describesSameAct(statementLower, req, forbiddenAction, options)) {
|
|
1509
|
+
const violation = findProhibitionViolation(statementLower, forbiddenAction, statementSubject, req.subject);
|
|
1039
1510
|
if (violation) {
|
|
1040
1511
|
conflicts.push({
|
|
1041
1512
|
requirement: req,
|
|
@@ -1065,7 +1536,12 @@ export function matchStatement(statement, requirements, options = {}) {
|
|
|
1065
1536
|
.sort((a, b) => b.score - a.score)
|
|
1066
1537
|
.slice(0, maxResults);
|
|
1067
1538
|
// Detect conflicts
|
|
1068
|
-
|
|
1539
|
+
// 突き合わせる相手は、一致した要件だけにする。RFC 全体を相手にしていたため、
|
|
1540
|
+
// `matchingRequirements` に出てこない要件が `conflicts` に入っていた。
|
|
1541
|
+
// 利用者は、示された矛盾の相手を一致の一覧から探せない。
|
|
1542
|
+
// 実測(RFC 67 本から要件文 300 件をそのまま主張として渡す):
|
|
1543
|
+
// `isValid: false` が 55 件 → 34 件。作った違反文 157 件の検出は 59 件のまま。
|
|
1544
|
+
const conflicts = detectConflicts(statement, matches.map((match) => match.requirement));
|
|
1069
1545
|
return {
|
|
1070
1546
|
matches,
|
|
1071
1547
|
conflicts,
|