@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.
Files changed (67) hide show
  1. package/README.ja.md +178 -50
  2. package/README.md +181 -51
  3. package/dist/cli/prefetch.d.ts +6 -1
  4. package/dist/cli/prefetch.d.ts.map +1 -1
  5. package/dist/cli/prefetch.js +53 -23
  6. package/dist/cli/prefetch.js.map +1 -1
  7. package/dist/config.d.ts +0 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +3 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.d.ts +16 -0
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/constants.js +80 -7
  14. package/dist/constants.js.map +1 -1
  15. package/dist/services/checklist-generator.d.ts.map +1 -1
  16. package/dist/services/checklist-generator.js +72 -7
  17. package/dist/services/checklist-generator.js.map +1 -1
  18. package/dist/services/rfc-fetcher.d.ts +19 -1
  19. package/dist/services/rfc-fetcher.d.ts.map +1 -1
  20. package/dist/services/rfc-fetcher.js +116 -19
  21. package/dist/services/rfc-fetcher.js.map +1 -1
  22. package/dist/services/rfc-service.d.ts +11 -1
  23. package/dist/services/rfc-service.d.ts.map +1 -1
  24. package/dist/services/rfc-service.js +45 -5
  25. package/dist/services/rfc-service.js.map +1 -1
  26. package/dist/services/rfc-text-parser.d.ts.map +1 -1
  27. package/dist/services/rfc-text-parser.js +1594 -88
  28. package/dist/services/rfc-text-parser.js.map +1 -1
  29. package/dist/services/rfcxml-parser.d.ts.map +1 -1
  30. package/dist/services/rfcxml-parser.js +366 -98
  31. package/dist/services/rfcxml-parser.js.map +1 -1
  32. package/dist/tools/definitions.d.ts +8 -0
  33. package/dist/tools/definitions.d.ts.map +1 -1
  34. package/dist/tools/definitions.js +17 -1
  35. package/dist/tools/definitions.js.map +1 -1
  36. package/dist/tools/handlers.d.ts +24 -12
  37. package/dist/tools/handlers.d.ts.map +1 -1
  38. package/dist/tools/handlers.js +161 -36
  39. package/dist/tools/handlers.js.map +1 -1
  40. package/dist/types/index.d.ts +22 -2
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/utils/cache.d.ts +19 -0
  43. package/dist/utils/cache.d.ts.map +1 -1
  44. package/dist/utils/cache.js +32 -0
  45. package/dist/utils/cache.js.map +1 -1
  46. package/dist/utils/disk-cache.d.ts +10 -6
  47. package/dist/utils/disk-cache.d.ts.map +1 -1
  48. package/dist/utils/disk-cache.js +13 -9
  49. package/dist/utils/disk-cache.js.map +1 -1
  50. package/dist/utils/logger.d.ts.map +1 -1
  51. package/dist/utils/logger.js +3 -1
  52. package/dist/utils/logger.js.map +1 -1
  53. package/dist/utils/requirement-extractor.d.ts.map +1 -1
  54. package/dist/utils/requirement-extractor.js +413 -21
  55. package/dist/utils/requirement-extractor.js.map +1 -1
  56. package/dist/utils/section.d.ts.map +1 -1
  57. package/dist/utils/section.js +11 -0
  58. package/dist/utils/section.js.map +1 -1
  59. package/dist/utils/statement-matcher.d.ts +69 -4
  60. package/dist/utils/statement-matcher.d.ts.map +1 -1
  61. package/dist/utils/statement-matcher.js +524 -48
  62. package/dist/utils/statement-matcher.js.map +1 -1
  63. package/dist/utils/text.d.ts +62 -0
  64. package/dist/utils/text.d.ts.map +1 -1
  65. package/dist/utils/text.js +273 -18
  66. package/dist/utils/text.js.map +1 -1
  67. package/package.json +5 -2
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * File layout (under `dir`):
10
10
  * ./rfc{N}.xml — raw RFCXML body, UTF-8
11
+ * ./rfc{N}.txt — raw RFC text body, UTF-8(`kind: 'text'` のとき)
11
12
  *
12
13
  * Intentionally minimal: no metadata, no eviction, no checksums. We trust the
13
14
  * filesystem and the source URL. If staleness becomes a concern, the operator
@@ -20,36 +21,39 @@
20
21
  import { promises as fs } from 'node:fs';
21
22
  import path from 'node:path';
22
23
  import { logger } from './logger.js';
24
+ const EXTENSION = { xml: '.xml', text: '.txt' };
23
25
  export class DiskCache {
24
26
  dir;
27
+ kind;
25
28
  /**
26
29
  * @param dir Absolute or relative directory path. Created lazily on first
27
30
  * `set` — readers tolerate non-existent dirs by returning null.
31
+ * @param kind 本文の種類。省略すると `xml`。
28
32
  */
29
- constructor(dir) {
33
+ constructor(dir, kind = 'xml') {
30
34
  this.dir = dir;
35
+ this.kind = kind;
31
36
  }
32
37
  /**
33
38
  * Construct a DiskCache from the `RFCXML_CACHE_DIR` environment variable.
34
39
  * Returns null when the var is unset/empty so the caller can keep the
35
40
  * disk-cache code path optional without conditionals at every call site.
36
41
  *
37
- * The cache is rooted at `<RFCXML_CACHE_DIR>/xml/` to leave room for
38
- * sibling subdirectories (e.g., text, metadata) in the future without
39
- * mixing files.
42
+ * The cache is rooted at `<RFCXML_CACHE_DIR>/<kind>/` so that XML と
43
+ * テキストが混ざらない。
40
44
  */
41
- static fromEnv() {
45
+ static fromEnv(kind = 'xml') {
42
46
  const raw = process.env.RFCXML_CACHE_DIR?.trim();
43
47
  if (!raw)
44
48
  return null;
45
- return new DiskCache(path.join(raw, 'xml'));
49
+ return new DiskCache(path.join(raw, kind), kind);
46
50
  }
47
51
  /**
48
52
  * Compute the on-disk filepath for a given RFC number. Public so the CLI
49
53
  * and tests can sanity-check.
50
54
  */
51
55
  filepath(rfcNumber) {
52
- return path.join(this.dir, `rfc${rfcNumber}.xml`);
56
+ return path.join(this.dir, `rfc${rfcNumber}${EXTENSION[this.kind]}`);
53
57
  }
54
58
  async has(rfcNumber) {
55
59
  try {
@@ -82,10 +86,10 @@ export class DiskCache {
82
86
  * and a write failure (e.g., disk full, permission denied) must not break
83
87
  * the runtime fetch path.
84
88
  */
85
- async set(rfcNumber, xml) {
89
+ async set(rfcNumber, body) {
86
90
  try {
87
91
  await fs.mkdir(this.dir, { recursive: true });
88
- await fs.writeFile(this.filepath(rfcNumber), xml, 'utf-8');
92
+ await fs.writeFile(this.filepath(rfcNumber), body, 'utf-8');
89
93
  }
90
94
  catch (error) {
91
95
  logger.warn(`DiskCache`, `write rfc${rfcNumber} failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -1 +1 @@
1
- {"version":3,"file":"disk-cache.js","sourceRoot":"","sources":["../../src/utils/disk-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,OAAO,SAAS;IAKQ;IAJ5B;;;OAGG;IACH,YAA4B,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAE3C;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAO;QACZ,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,CAAC;QACjD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,OAAO,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED;;;OAGG;IACH,QAAQ,CAAC,SAAiB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAiB;QACzB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;YAC1C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAC,SAAiB;QACzB,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,sDAAsD;YACtD,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,MAAM,CAAC,IAAI,CACT,WAAW,EACX,WAAW,SAAS,YAAY,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACzF,CAAC;YACJ,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,GAAG,CAAC,SAAiB,EAAE,GAAW;QACtC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9C,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CACT,WAAW,EACX,YAAY,SAAS,YAAY,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAC1F,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"disk-cache.js","sourceRoot":"","sources":["../../src/utils/disk-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAKrC,MAAM,SAAS,GAA8B,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAE3E,MAAM,OAAO,SAAS;IAOF;IACA;IAPlB;;;;OAIG;IACH,YACkB,GAAW,EACX,OAAkB,KAAK;QADvB,QAAG,GAAH,GAAG,CAAQ;QACX,SAAI,GAAJ,IAAI,CAAmB;IACtC,CAAC;IAEJ;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,CAAC,OAAkB,KAAK;QACpC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,CAAC;QACjD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,OAAO,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;IACnD,CAAC;IAED;;;OAGG;IACH,QAAQ,CAAC,SAAiB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAiB;QACzB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;YAC1C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAC,SAAiB;QACzB,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,sDAAsD;YACtD,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,MAAM,CAAC,IAAI,CACT,WAAW,EACX,WAAW,SAAS,YAAY,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACzF,CAAC;YACJ,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,GAAG,CAAC,SAAiB,EAAE,IAAY;QACvC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9C,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CACT,WAAW,EACX,YAAY,SAAS,YAAY,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAC1F,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D;;GAEG;AACH,UAAU,MAAM;IACd,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CAC9D;AASD;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,MAqBpB,CAAC"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D;;GAEG;AACH,UAAU,MAAM;IACd,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CAC9D;AASD;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,MAuBpB,CAAC"}
@@ -13,9 +13,11 @@ function formatMessage(level, context, message) {
13
13
  * Can be replaced with more sophisticated logging (e.g., winston, pino) in the future
14
14
  */
15
15
  export const logger = {
16
+ // stdio トランスポートでは stdout が JSON-RPC の線なので、すべて stderr に書く。
17
+ // `console.debug` は stdout に出るため、DEBUG=1 で使った途端に線が壊れる(Issue #19)。
16
18
  debug(context, message) {
17
19
  if (process.env.DEBUG) {
18
- console.debug(formatMessage('debug', context, message));
20
+ console.error(formatMessage('debug', context, message));
19
21
  }
20
22
  },
21
23
  info(context, message) {
@@ -1 +1 @@
1
- {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiBH;;GAEG;AACH,SAAS,aAAa,CAAC,KAAe,EAAE,OAAe,EAAE,OAAe;IACtE,OAAO,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAW;IAC5B,KAAK,CAAC,OAAe,EAAE,OAAe;QACpC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,OAAe;QACnC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,OAAe;QACnC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,OAAe,EAAE,KAAa;QACnD,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QACxD,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiBH;;GAEG;AACH,SAAS,aAAa,CAAC,KAAe,EAAE,OAAe,EAAE,OAAe;IACtE,OAAO,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAW;IAC5B,0DAA0D;IAC1D,kEAAkE;IAClE,KAAK,CAAC,OAAe,EAAE,OAAe;QACpC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,OAAe;QACnC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,OAAe;QACnC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,OAAe,EAAE,KAAa;QACnD,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QACxD,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;CACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"requirement-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/requirement-extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAUhF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB;IAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gCAAgC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iBAAiB;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAsCD;;GAEG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,OAAO,EAAE,EACnB,MAAM,CAAC,EAAE,iBAAiB,EAC1B,OAAO,GAAE,YAAwC,GAChD,WAAW,EAAE,CA+Gf"}
1
+ {"version":3,"file":"requirement-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/requirement-extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAWhF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB;IAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gCAAgC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iBAAiB;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAsCD;;GAEG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,OAAO,EAAE,EACnB,MAAM,CAAC,EAAE,iBAAiB,EAC1B,OAAO,GAAE,YAAwC,GAChD,WAAW,EAAE,CA+Tf"}
@@ -2,7 +2,7 @@
2
2
  * 要件抽出の共通ユーティリティ
3
3
  * XML/テキストパーサー共通で使用
4
4
  */
5
- import { clipAtClauseEnd, extractSentence, foldWhitespace, requirementSource, stripListMarker, } from './text.js';
5
+ import { clipAtClauseEnd, extractRequirementMarkers, extractSentence, foldWhitespace, requirementSource, stripListMarker, } from './text.js';
6
6
  import { normalizeSectionNumber } from './section.js';
7
7
  /**
8
8
  * セクション番号を正規化(`section-6.2.3` → `6.2.3`、`section-appendix.a.2.5` → `A.2.5`)
@@ -40,7 +40,23 @@ function matchesSectionFilter(sectionId, filter) {
40
40
  */
41
41
  export function extractRequirementsFromSections(sections, filter, options = { parseComponents: true }) {
42
42
  const requirements = [];
43
- let idCounter = 1;
43
+ /**
44
+ * 要件の id の連番。**節ごとに数える。**
45
+ *
46
+ * 文書全体で 1 本の連番にしていたため、同じ要件が呼び方で違う id を持って
47
+ * いた。`validate_statement` は全件から取るので RFC 9110 §6.6.1 の禁止を
48
+ * `R-6.6.1-76` と報告するが、利用者がそれを読もうと
49
+ * `get_requirements({ rfc: 9110, section: "6.6.1" })` を呼ぶと、返るのは
50
+ * `R-6.6.1-1` 〜 `R-6.6.1-7` で、**教えられた id が存在しない**。
51
+ *
52
+ * 節ごとに数えれば「その節の n 番目」という意味になり、絞り込みで変わらない。
53
+ */
54
+ const idCounters = new Map();
55
+ const nextId = (sectionId) => {
56
+ const next = (idCounters.get(sectionId) ?? 0) + 1;
57
+ idCounters.set(sectionId, next);
58
+ return `R-${sectionId}-${next}`;
59
+ };
44
60
  /**
45
61
  * 出力済みの要件を記録する。キーは「セクション + レベル + 要件文」。
46
62
  *
@@ -56,6 +72,84 @@ export function extractRequirementsFromSections(sections, filter, options = { pa
56
72
  seen.add(key);
57
73
  return false;
58
74
  }
75
+ /**
76
+ * その位置のキーワードが、**行為を求めているのではなく語として名指されて**
77
+ * いるか。
78
+ *
79
+ * BCP 14 のキーワードは名詞にもなる。RFC 2119 §7 は
80
+ *
81
+ * > The effects on security of not implementing **a MUST or SHOULD**, or doing
82
+ * > something the specification says MUST NOT or SHOULD NOT be done, are very
83
+ * > subtle.
84
+ *
85
+ * と書く。この 1 文から MUST / SHOULD / MUST NOT / SHOULD NOT の 4 件が立って
86
+ * いた。RFC 9051 の付録 E は `(Changed from a SHOULD to a MUST.)`、RFC 5246 §1.2 は
87
+ * `Support for the SSLv2 backward-compatible hello is now a MAY, not a SHOULD,
88
+ * with sending it a SHOULD NOT.` である。
89
+ *
90
+ * 形容詞として名詞に付く `an OPTIONAL feature` `the RECOMMENDED default values`
91
+ * も同じで、その節の実装が何かをすることを求めていない。
92
+ *
93
+ * **冠詞が直前に来ることだけ**を見る。引用符付き(`"MUST"`)は `B10` が、
94
+ * BCP 14 の定型文は `B9` が既に落としている。
95
+ *
96
+ * 実測(RFC 82 本): 48 件。うち 9 件は 1 つの文から複数のレベルが立っていた
97
+ * もので、名指しでないほうのキーワードは残る(RFC 9051 §6.2.2 の
98
+ * `It MAY also negotiate an OPTIONAL security layer` は MAY が残り
99
+ * OPTIONAL が落ちる)。
100
+ */
101
+ const ARTICLE_BEFORE_KEYWORD = /\b(?:an?|the)\s+$/i;
102
+ /** BCP 14 のキーワード。名指しかどうかを見るために、位置から読み直す。 */
103
+ const KEYWORD_AT_POSITION = /^(?:MUST NOT|SHALL NOT|SHOULD NOT|NOT RECOMMENDED|MUST|SHALL|SHOULD|MAY|REQUIRED|RECOMMENDED|OPTIONAL)/;
104
+ /**
105
+ * 名詞として置かれたキーワードの後ろに来るもの。
106
+ *
107
+ * 冠詞だけを見ると、**形容詞として名詞に付く**書き方まで落ちる。
108
+ * `an OPTIONAL feature of HTTP`(RFC 9110 §14)、
109
+ * `the RECOMMENDED default values for these two parameters are 3.5 seconds …`
110
+ * (RFC 3550 §6.2)、`A RECOMMENDED mechanism to achieve this is …`
111
+ * (RFC 3261 §16.7)は、その節が何を選ぶべきかを述べており要件である。
112
+ *
113
+ * 名指しのときはキーワードの後ろに名詞が来ない。
114
+ * `a MUST or SHOULD` / `a MAY, not a SHOULD` / `a SHOULD to a MUST.`
115
+ */
116
+ const KEYWORD_AS_BARE_NOUN = /^\s*(?:[,.;:)"'\u201d]|$|(?:or|and|to|not|nor|than|instead|rather|level|keyword|in|of|for|by|with|from|at|on|into)\b)/i;
117
+ function namesTheKeyword(text, position) {
118
+ if (!ARTICLE_BEFORE_KEYWORD.test(text.slice(Math.max(0, position - 12), position)))
119
+ return false;
120
+ const keyword = KEYWORD_AT_POSITION.exec(text.slice(position));
121
+ if (!keyword)
122
+ return false;
123
+ return KEYWORD_AS_BARE_NOUN.test(text.slice(position + keyword[0].length));
124
+ }
125
+ /**
126
+ * 語を並べただけで、文になっていないか。
127
+ *
128
+ * RFC 2578 §3.7 は ASN.1 の予約語 96 個を字下げして並べる。
129
+ *
130
+ * ```
131
+ * ABSENT ACCESS AGENT-CAPABILITIES ANY APPLICATION AUGMENTS BEGIN
132
+ * BIT BITS BOOLEAN BY CHOICE COMPONENT COMPONENTS CONTACT-INFO
133
+ * …
134
+ * OPTIONAL ORGANIZATION Opaque PLUS-INFINITY PRESENT PRIVATE
135
+ * ```
136
+ *
137
+ * この中の `OPTIONAL` から要件が 1 件立ち、チェックリストの 1 項目が
138
+ * **903 文字の語の並び**になっていた。
139
+ *
140
+ * 小文字だけの語が 1 つも無いことで見分ける。地の文は `the` `of` `to` `is` の
141
+ * ような語を必ず含む。予約語の一覧は `BY` `FROM` `OF` `WITH` のように大文字で
142
+ * 書かれるので、これらは小文字の語として数えない。
143
+ *
144
+ * 実測(RFC 97 本): 1 件。
145
+ */
146
+ const LOWERCASE_WORD = /(?:^|\s)[a-z][a-z']*(?:\s|$)/;
147
+ function looksLikeWordList(sentence) {
148
+ const words = sentence.trim().split(/\s+/).filter(Boolean);
149
+ if (words.length < 4)
150
+ return false;
151
+ return !LOWERCASE_WORD.test(` ${sentence} `);
152
+ }
59
153
  function processSection(section, path) {
60
154
  // 内部の識別子(RFCXML の `pn`)はそのままでは外に出せない。出力する
61
155
  // `id` と `section` は節番号にそろえる。
@@ -67,9 +161,6 @@ export function extractRequirementsFromSections(sections, filter, options = { pa
67
161
  for (const block of section.content) {
68
162
  if (block.type === 'text' && block.requirements.length > 0) {
69
163
  for (const marker of block.requirements) {
70
- if (filter?.level && marker.level !== filter.level) {
71
- continue;
72
- }
73
164
  // 図・ABNF は空白を畳まない。ABNF の注釈は散文として組み直す。
74
165
  const source = requirementSource(block.content, marker.position, marker.level);
75
166
  const raw = extractSentence(source.text, source.position);
@@ -79,11 +170,38 @@ export function extractRequirementsFromSections(sections, filter, options = { pa
79
170
  if (isDuplicate(sectionId, marker.level, sentence)) {
80
171
  continue;
81
172
  }
82
- const components = options.parseComponents
83
- ? parseRequirementComponents(sentence, marker.level)
173
+ // キーワードのほかに語が無いものは要件文ではない。
174
+ // RFC 5280 §11.2 の ASN.1 の切れ端 `"OPTIONAL,"` `"} OPTIONAL,"` が
175
+ // 要件として出ていた(RFC 90 本・要件 9,584 件のうち 4 件)。
176
+ if (!hasSubstance(sentence)) {
177
+ continue;
178
+ }
179
+ // キーワードが名詞として置かれているものは要件ではない。
180
+ if (namesTheKeyword(block.content, marker.position)) {
181
+ continue;
182
+ }
183
+ // 語を並べただけの塊は文ではない。
184
+ if (looksLikeWordList(sentence)) {
185
+ continue;
186
+ }
187
+ // 図・表の行から取った要件には主語も条件もアクションも無い。
188
+ // RFC 2131 §4.3.1 の表の行 "Message SHOULD SHOULD SHOULD" に
189
+ // `subject: "message should"` `action: "SHOULD SHOULD"` が付いていた。
190
+ // **id はレベルで絞る前に進める。** `filter.level` の `continue` が
191
+ // `nextId()` より前にあったため、レベルで絞るとその節の中で一致する
192
+ // ものだけを 1 から数え直していた。RFC 6455 §5.1 の R-5.1-6(MAY)が
193
+ // `level: "MAY"` では R-5.1-2 になり、`validate_statement` が教えた
194
+ // id を `get_requirements` で引けなかった。重複排除やキーワードの
195
+ // 名指しなど、レベルによらない判定はここより前に置くこと。
196
+ const id = nextId(sectionId);
197
+ if (filter?.level && marker.level !== filter.level) {
198
+ continue;
199
+ }
200
+ const components = options.parseComponents && source.prose
201
+ ? parseRequirementComponents(sentence, marker.level, block.content)
84
202
  : {};
85
203
  requirements.push({
86
- id: `R-${sectionId}-${idCounter++}`,
204
+ id,
87
205
  level: marker.level,
88
206
  text: sentence,
89
207
  section: sectionId,
@@ -97,27 +215,92 @@ export function extractRequirementsFromSections(sections, filter, options = { pa
97
215
  });
98
216
  }
99
217
  }
218
+ // 表の行からも抽出する。
219
+ //
220
+ // 図・表の行にキーワードが当たったときは、**その 1 行だけ**を要件文にする
221
+ // (テキスト経路の `requirementSource` と同じ規則)。XML 経路の `<table>` は
222
+ // 行が `<tr>` で分かれているので、行のセルを " | " で繋いだものを 1 行とする。
223
+ // 見出しの行は見ない。RFC 9293 §3.11 の要件一覧表は見出しに
224
+ // MUST / SHOULD / MAY を並べており、それは要件ではない。
225
+ // 表の行から取った要件には主語も条件もアクションも無い。
226
+ //
227
+ // 要求 ID ラベル(`MUST-14`)だけの行は要件にしない。RFC 9293 の Appendix B は
228
+ // 本文の要件を `Treat as unsigned number | MUST-1 | X | | | |` の形で 110 行
229
+ // 並べる。行はラベルでレベルを名乗るが、実際のレベルは X の列が示すもので、
230
+ // `MUST-60` の行の X は MUST NOT の列にある。要件そのものは §3.1 などの本文から
231
+ // 既に出ている。
232
+ if (block.type === 'table') {
233
+ const headerLine = block.headers.join(' | ');
234
+ for (const row of block.rows) {
235
+ const line = foldWhitespace(row.join(' | '));
236
+ for (const marker of extractRequirementMarkers(line)) {
237
+ const level = marker.level;
238
+ if (filter?.level && level !== filter.level) {
239
+ continue;
240
+ }
241
+ if (/^-\d/.test(line.slice(marker.position + level.length))) {
242
+ continue;
243
+ }
244
+ if (isDuplicate(sectionId, level, line)) {
245
+ continue;
246
+ }
247
+ if (!hasSubstance(line) || looksLikeWordList(line)) {
248
+ continue;
249
+ }
250
+ if (namesTheKeyword(line, marker.position)) {
251
+ continue;
252
+ }
253
+ requirements.push({
254
+ id: nextId(sectionId),
255
+ level,
256
+ text: line,
257
+ section: sectionId,
258
+ sectionTitle: section.title,
259
+ fullContext: headerLine ? `${headerLine}\n${line}` : line,
260
+ });
261
+ }
262
+ }
263
+ }
100
264
  // リストアイテムからも抽出
101
265
  if (block.type === 'list') {
102
266
  for (const item of block.items) {
103
267
  for (const marker of item.requirements) {
104
- if (filter?.level && marker.level !== filter.level) {
268
+ // 箇条書きの項目も、文の単位で切り出す。
269
+ //
270
+ // 箇条書きの項目は 1 文であることが多く、v0.6.14 まではその前提で
271
+ // 項目の全文を要件文にしていた。RFCXML の `<dl>` は 1 項目が段落に
272
+ // なる。RFC 9113 §8.3.1 の `":authority"` の項目は 2,150 文字の
273
+ // 段落で、その中に MUST・MUST NOT・SHOULD・MAY が入っているため、
274
+ // 同じ 2,150 文字が 4 件の要件として並んでいた。
275
+ const itemContext = foldWhitespace(stripListMarker(item.content));
276
+ const itemText = foldWhitespace(stripListMarker(extractSentence(item.content, marker.position)));
277
+ if (isDuplicate(sectionId, marker.level, itemText)) {
105
278
  continue;
106
279
  }
107
- const itemText = foldWhitespace(stripListMarker(item.content));
108
- if (isDuplicate(sectionId, marker.level, itemText)) {
280
+ if (!hasSubstance(itemText)) {
281
+ continue;
282
+ }
283
+ if (namesTheKeyword(item.content, marker.position)) {
284
+ continue;
285
+ }
286
+ if (looksLikeWordList(itemText)) {
287
+ continue;
288
+ }
289
+ // テキストブロックと同じく、id を進めてからレベルで絞る。
290
+ const id = nextId(sectionId);
291
+ if (filter?.level && marker.level !== filter.level) {
109
292
  continue;
110
293
  }
111
294
  const components = options.parseComponents
112
- ? parseRequirementComponents(itemText, marker.level)
295
+ ? parseRequirementComponents(itemText, marker.level, itemContext)
113
296
  : {};
114
297
  requirements.push({
115
- id: `R-${sectionId}-${idCounter++}`,
298
+ id,
116
299
  level: marker.level,
117
300
  text: itemText,
118
301
  section: sectionId,
119
302
  sectionTitle: section.title,
120
- fullContext: itemText,
303
+ fullContext: itemContext,
121
304
  ...components,
122
305
  });
123
306
  }
@@ -135,20 +318,95 @@ export function extractRequirementsFromSections(sections, filter, options = { pa
135
318
  }
136
319
  return requirements;
137
320
  }
321
+ /**
322
+ * 要件文に、キーワード以外の語があるか。
323
+ *
324
+ * ASN.1 の切れ端が要件として出ることがある。RFC 5280 §11.2 の
325
+ * `OPTIONAL,` や `} OPTIONAL,` は、キーワードを外すと何も残らない。
326
+ * 要件は「誰が何をするか」を書いた文であって、キーワード単体ではない。
327
+ */
328
+ function hasSubstance(text) {
329
+ const withoutKeywords = text.replace(/\b(?:MUST|SHALL|SHOULD|MAY|REQUIRED|OPTIONAL|RECOMMENDED|NOT)\b/g, ' ');
330
+ return /[A-Za-z0-9]/.test(withoutKeywords);
331
+ }
138
332
  /**
139
333
  * 要件文から構成要素を解析
140
334
  */
141
- function parseRequirementComponents(text, level) {
335
+ function parseRequirementComponents(text, level, context) {
142
336
  const result = {};
143
337
  // 主語の抽出("The client MUST" → "client")
144
- const subjectMatch = text.match(/^(?:The\s+)?(\w+(?:\s+\w+)?)\s+(?:MUST|SHALL|SHOULD|MAY)/i);
338
+ //
339
+ // **文頭に固定しない。** RFC の要件文は前置きから始まることが多い。
340
+ //
341
+ // - `(Note that masking is done …) The server MUST close the connection …`
342
+ // - `In this case, a server MAY send a Close frame …`
343
+ // - `Because of the potential for trailer fields to be discarded, a server …`
344
+ //
345
+ // 文頭で探すと、これらの主語が取れない。実測(RFC 64 本・要件 9,684 件)で
346
+ // `subject` が付くのは 27.9% だけだった。キーワードの直前から取ると 91.8%。
347
+ //
348
+ // `subject` は `generate_checklist` の `role` の絞り込みにも使う。取れないと
349
+ // 絞り込みが効かず、`role: "client"` にサーバの要件が 865 件(8.9%)残っていた。
350
+ // 冠詞のあとには**必ず空白がある**。`\\s*` にすると、冠詞の候補が語の頭の
351
+ // 1 文字を食う。"Automated clients MUST" の "A" が冠詞として消費され、
352
+ // 主語が "utomated clients" になっていた(実測で 600 件、6.20%)。
353
+ //
354
+ // 直前の語が接続詞や代名詞のことがある。1 つの文に要件が 2 つあると、
355
+ // 2 つ目のキーワードの手前が "and" になる(RFC 9110 §4.3.4 の
356
+ // "MUST log the error to an appropriate audit log (if available) and MUST
357
+ // provide …")。代名詞(it / this / they)も同じで、指しているものは
358
+ // 文の前の方にある。**主語ではないので、名乗らせない。**
359
+ //
360
+ // **キーワードは原文どおりの大文字で、否定形の一部を除いて照合する。**
361
+ // `\s+MUST\b` は `MUST NOT` の `MUST` にも当たる。RFC 8446 §4.5 の
362
+ // "Servers MUST NOT send this message, and clients receiving it MUST
363
+ // terminate the connection …" は、level `MUST` の要件なのに主語が servers、
364
+ // action が "NOT send this message" になり、`generate_checklist` の
365
+ // `role: "client"` から落ちていた。`i` フラグも外す。RFC 8446 §D.4 の
366
+ // "as they must be ignored" や RFC 6455 §7.2.3 の "Should the first reconnect
367
+ // attempt fail" は BCP 14 のキーワードではない。`requiredActionOf()` と同じ形。
368
+ //
369
+ // 主語とキーワードのあいだに「分詞 + 代名詞」の関係節が挟まることがある
370
+ // ("clients receiving it MUST")。その 2 語は主語ではないので読み飛ばす。
371
+ const keywordPattern = `\\b${level.replace(' ', '\\s+')}\\b${level.endsWith('NOT') ? '' : '(?!\\s+NOT)'}`;
372
+ const subjectMatch = new RegExp(`\\b(?:(?:[Tt]he|[Aa]n?|[Ee]ach|[Ee]very|[Aa]ll)\\s+)?([A-Za-z][\\w-]*(?:\\s+[A-Za-z][\\w-]*)?)(?:\\s+[A-Za-z][\\w-]*ing\\s+(?:it|them|this|these|those|one))?\\s+${keywordPattern}`).exec(text);
145
373
  if (subjectMatch) {
146
- result.subject = subjectMatch[1].toLowerCase();
374
+ // 2 語の取り込みが冠詞ごと拾うことがある("of a client MUST" → "a client")。
375
+ const subject = subjectMatch[1]
376
+ .toLowerCase()
377
+ .replace(/^(?:the|a|an|each|every|all)\s+/, '')
378
+ .trim();
379
+ if (subject && PRONOUN_SUBJECT.has(subject)) {
380
+ // 代名詞が指しているものは、同じ段落の前の文にある。
381
+ const antecedent = context ? subjectBeforeSentence(context, text) : undefined;
382
+ if (antecedent)
383
+ result.subject = antecedent;
384
+ }
385
+ else {
386
+ // 2 語の取り込みが機能語を巻き込むことがある("response and MUST" →
387
+ // "response and"、"methods are REQUIRED" → "methods are")。前後の機能語を
388
+ // 落として、残りを主語とする。全部落ちたら主語は無い。
389
+ //
390
+ // 直前が接続詞なら、キーワードは前の節と主語を共有している。RFC 9110
391
+ // §10.2.4 の "An origin server SHOULD NOT generate … detail and SHOULD
392
+ // limit …" の SHOULD の主語は origin server であって detail ではない。
393
+ // 同じ文の手前にある、キーワードの直前の語を採る。
394
+ const coordinated = /\b(?:and|or|but|nor)$/.test(subject);
395
+ const shared = coordinated ? lastSubjectBefore(text.slice(0, subjectMatch.index)) : undefined;
396
+ const trimmed = shared ?? trimFunctionWords(subject);
397
+ if (trimmed)
398
+ result.subject = trimmed;
399
+ }
147
400
  }
148
- // 条件の抽出("if", "when", "unless")
401
+ // 条件の抽出("if", "when", "where", "in case")
402
+ //
403
+ // `unless` はここに入れない。例外の側と重なり、`condition` と `exception` に
404
+ // 同じ文字列が入る。実測(RFC 49 本・要件 7,797 件)で 247 件(3.2%)あった。
405
+ // 「X でない限り」は条件ではなく例外である。
406
+ //
149
407
  // 切り出しは clipAtClauseEnd に任せる。`[^,.]+` で止めると、括弧内のカンマや
150
408
  // 節番号のピリオドで切れて "this fails (e" のようになる。
151
- const conditionMatch = text.match(/\b(if|when|unless|where|in case)\s+(.+)/is);
409
+ const conditionMatch = text.match(/\b(if|when|where|in case)\s+(.+)/is);
152
410
  if (conditionMatch) {
153
411
  const condition = clipAtClauseEnd(conditionMatch[2]);
154
412
  if (condition)
@@ -161,8 +419,8 @@ function parseRequirementComponents(text, level) {
161
419
  if (exception)
162
420
  result.exception = exception;
163
421
  }
164
- // アクションの抽出(キーワードの後)
165
- const actionMatch = text.match(new RegExp(`${level}\\s+(.+)`, 'is'));
422
+ // アクションの抽出(キーワードの後)。主語と同じく否定形の一部と小文字は除く。
423
+ const actionMatch = text.match(new RegExp(`${keywordPattern}\\s+(.+)`, 's'));
166
424
  if (actionMatch) {
167
425
  const action = clipAtClauseEnd(actionMatch[1]);
168
426
  if (action)
@@ -170,4 +428,138 @@ function parseRequirementComponents(text, level) {
170
428
  }
171
429
  return result;
172
430
  }
431
+ /**
432
+ * 代名詞が指しているものを、同じ段落の前の文から取る。
433
+ *
434
+ * RFC 6455 §5.1 は
435
+ *
436
+ * > A client MUST close a connection if it detects a masked frame.
437
+ * > **In this case, it MAY use the status code 1002** (protocol error) …
438
+ *
439
+ * と書く。`it` は client を指す。取れないと `generate_checklist` の
440
+ * `role: "server"` にもこの項目が出る(主語が無ければ本文を見るが、本文にも
441
+ * client / server の語が無いため両方に残る)。
442
+ *
443
+ * 前の文の中で、キーワードの直前に置かれた語を採る。要件文と同じ取り方である。
444
+ */
445
+ function subjectBeforeSentence(context, sentence) {
446
+ // **文の全体で探す。** 頭の数十文字で探すと、同じ書き出しの文が段落に 2 つ
447
+ // あるときに手前の方に当たる。RFC 6455 §5.1 は "In this case, …" を 2 回書く
448
+ // ので、40 文字で探すと 1 つ目に当たり、引き継ぐ主語が server になっていた
449
+ // (正しくは直前の文の client)。
450
+ //
451
+ // 段落と文で空白の畳み方が違う(段落は改行を含み、文は切り出したまま)。
452
+ // 両方を畳んでから探す。
453
+ const folded = foldWhitespace(context);
454
+ const at = folded.indexOf(foldWhitespace(sentence));
455
+ if (at <= 0)
456
+ return undefined;
457
+ return lastSubjectBefore(folded.slice(0, at));
458
+ }
459
+ /**
460
+ * 与えた文字列の中で、キーワードの直前に置かれた語のうち最後のもの。
461
+ *
462
+ * 冠詞は大文字で始まることがある(文頭の "A client MUST …")。小文字だけで
463
+ * 書くと、文頭の文が当たらず、その手前の文の主語を引き継いでいた。
464
+ */
465
+ function lastSubjectBefore(before) {
466
+ let found;
467
+ const pattern = /\b(?:(?:[Tt]he|[Aa]n?|[Ee]ach|[Ee]very|[Aa]ll)\s+)?([A-Za-z][\w-]*(?:\s+[A-Za-z][\w-]*)?)\s+(?:MUST|SHALL|SHOULD|MAY|REQUIRED|RECOMMENDED|OPTIONAL)\b/g;
468
+ let match;
469
+ while ((match = pattern.exec(before)) !== null) {
470
+ const candidate = match[1].toLowerCase().trim();
471
+ // 2 語の取り込みが接続詞を巻き込むことがある("requests and MUST" →
472
+ // "requests and")。1 語でも機能語が混じっていたら主語ではない。
473
+ if (candidate.split(/\s+/).some((word) => NOT_A_SUBJECT.has(word)))
474
+ continue;
475
+ found = candidate;
476
+ }
477
+ return found;
478
+ }
479
+ /**
480
+ * 前後の機能語を落とす。残らなければ `undefined`。
481
+ *
482
+ * 主語はキーワードの直前 1〜2 語から取るので、機能語を巻き込むことがある。
483
+ * "An origin server MUST NOT generate a Date header field" の 2 語は
484
+ * "response and"(RFC 9110 §10.2.1)や "methods are"(同 §9.1)になる。
485
+ * 落とすと "response" "methods" が残る。両方とも機能語なら主語ではない。
486
+ */
487
+ function trimFunctionWords(subject) {
488
+ const words = subject.split(/\s+/).filter((word) => word.length > 0);
489
+ while (words.length > 0 && NOT_A_SUBJECT.has(words[words.length - 1]))
490
+ words.pop();
491
+ while (words.length > 0 && NOT_A_SUBJECT.has(words[0]))
492
+ words.shift();
493
+ return words.length > 0 ? words.join(' ') : undefined;
494
+ }
495
+ /** 前の文から引き継ぐ代名詞。 */
496
+ const PRONOUN_SUBJECT = new Set(['it', 'they', 'them', 'this', 'these', 'those', 'he', 'she']);
497
+ /** 主語として認めない語。接続詞・代名詞・関係詞。 */
498
+ const NOT_A_SUBJECT = new Set([
499
+ 'and',
500
+ 'or',
501
+ 'but',
502
+ 'nor',
503
+ 'so',
504
+ 'then',
505
+ 'it',
506
+ 'its',
507
+ 'this',
508
+ 'that',
509
+ 'these',
510
+ 'those',
511
+ 'they',
512
+ 'them',
513
+ 'we',
514
+ 'you',
515
+ 'he',
516
+ 'she',
517
+ 'which',
518
+ 'who',
519
+ 'whom',
520
+ 'there',
521
+ 'here',
522
+ 'if',
523
+ 'when',
524
+ 'while',
525
+ 'also',
526
+ 'thus',
527
+ 'hence',
528
+ 'however',
529
+ 'therefore',
530
+ 'otherwise',
531
+ // 冠詞・前置詞・繋辞。1 語だけ取れたときに残ることがある。
532
+ 'the',
533
+ 'a',
534
+ 'an',
535
+ 'of',
536
+ 'to',
537
+ 'in',
538
+ 'on',
539
+ 'at',
540
+ 'by',
541
+ 'as',
542
+ 'for',
543
+ 'from',
544
+ 'with',
545
+ 'is',
546
+ 'are',
547
+ 'was',
548
+ 'were',
549
+ 'be',
550
+ 'been',
551
+ 'being',
552
+ 'has',
553
+ 'have',
554
+ 'had',
555
+ 'not',
556
+ 'no',
557
+ 'all',
558
+ 'any',
559
+ 'each',
560
+ 'every',
561
+ 'such',
562
+ 'other',
563
+ 'both',
564
+ ]);
173
565
  //# sourceMappingURL=requirement-extractor.js.map