peaks-loop 4.0.49 → 4.0.51
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/CHANGELOG.md +38 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -0
- package/dist/cli/commands/hooks-commands.js +55 -38
- package/dist/cli/commands/share-commands.js +113 -20
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/dispatch/sub-agent-dispatcher.d.ts +11 -30
- package/dist/services/dispatch/sub-agent-dispatcher.js +5 -48
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/ide/adapters/claude-code-adapter.js +0 -1
- package/dist/services/ide/adapters/codex-adapter.js +1 -2
- package/dist/services/ide/adapters/cursor-adapter.js +1 -2
- package/dist/services/ide/adapters/hermes-adapter.js +1 -2
- package/dist/services/ide/adapters/openclaw-adapter.js +1 -2
- package/dist/services/ide/adapters/qoder-adapter.js +1 -2
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +1 -2
- package/dist/services/ide/adapters/trae-adapter.js +1 -2
- package/dist/services/ide/adapters/zcode-adapter.js +0 -1
- package/dist/services/ide/ide-types.d.ts +0 -2
- package/dist/services/observability/observability-service.d.ts +1 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-perf-audit/SKILL.md +2 -2
- package/skills/bee/peaks-perf-audit/references/audit-protocol.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +3 -3
- package/skills/bee/peaks-prd/references/prd-for-multi-pass.md +1 -1
- package/skills/bee/peaks-prd/references/workflow.md +1 -1
- package/skills/bee/peaks-qa/SKILL.md +6 -7
- package/skills/bee/peaks-qa/references/external-capability-guidance.md +1 -1
- package/skills/bee/peaks-qa/references/qa-fanout-contract.md +1 -1
- package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +2 -2
- package/skills/bee/peaks-rd/SKILL.md +2 -3
- package/skills/bee/peaks-rd/references/code-reviewer-4dim-hint.md +1 -1
- package/skills/bee/peaks-rd/references/external-references.md +1 -1
- package/skills/bee/peaks-rd/references/mandatory-perf-baseline.md +1 -1
- package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +2 -2
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +11 -8
- package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +7 -7
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
- package/skills/bee/peaks-rd/references/reading-v2-slice-results.md +1 -1
- package/skills/bee/peaks-rd/references/v2-12-fanout-collapse.md +7 -5
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +3 -3
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +3 -3
- package/skills/bee/peaks-security-audit/references/audit-protocol.md +1 -1
- package/skills/bee/peaks-txt/references/context-capsule.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +20 -18
- package/skills/peaks-code/references/context-governance.md +1 -1
- package/skills/peaks-code/references/dag-orchestrator.md +3 -4
- package/skills/peaks-code/references/external-references.md +1 -1
- package/skills/peaks-code/references/external-skill-invocation.md +2 -2
- package/skills/peaks-code/references/fanout-mandatory.md +3 -3
- package/skills/peaks-code/references/frontend-only-mode.md +2 -2
- package/skills/peaks-code/references/gstack-integration.md +1 -1
- package/skills/peaks-code/references/micro-cycle.md +1 -1
- package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
- package/skills/peaks-code/references/project-scan-checklist.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +1 -1
- package/skills/peaks-code/references/runbook.md +6 -3
- package/skills/peaks-code/references/session-overload-signal-index.md +6 -4
- package/skills/peaks-code/references/startup-sequence.md +17 -17
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/step-11-memory-sediment.md +2 -2
- package/skills/peaks-code/references/sub-agent-dispatch.md +26 -25
- package/skills/peaks-code/references/swarm-dispatch-contract.md +1 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +3 -3
- package/skills/peaks-code/references/worktree-governance.md +1 -1
- package/skills/peaks-final-review/SKILL.md +3 -3
- package/skills/peaks-ide/references/audit-log-helper.md +5 -4
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +4 -4
- package/skills/peaks-slice-decompose/references/cross-pass-edge-interpretation.md +1 -1
- package/skills/peaks-slice-decompose/references/granularity-decision.md +1 -1
- package/skills/peaks-slice-decompose/references/v2-schema.md +2 -2
- package/skills/peaks-solo/SKILL.md +1 -2
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- package/skills/peaks-code/references/command-migration.md +0 -3
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* Backing detector — classifies each red line as `cli-backed`, `partial`,
|
|
3
3
|
* or `prose-only`. The classifier already sets the backing for catalog hits
|
|
4
4
|
* (cli-backed when an enforcer file path is present). This module exists to
|
|
5
|
-
* handle the post-classification
|
|
6
|
-
* (a gate exists but the LLM can bypass it)
|
|
7
|
-
*
|
|
5
|
+
* handle the post-classification nuances: heuristics for the "partial" tier
|
|
6
|
+
* (a gate exists but the LLM can bypass it), verification that the enforcer
|
|
7
|
+
* file exists on disk, and — since A9 of the 2026-09-15 diagnosis —
|
|
8
|
+
* verification that the enforcer is actually reachable from a call site.
|
|
9
|
+
*
|
|
10
|
+
* `cli-backed` means "a CLI surface runs this rule". A file that exists but
|
|
11
|
+
* that nothing imports runs nothing, so it is `prose-only` with the same
|
|
12
|
+
* weight as an unwritten rule. Callers pass the live set from
|
|
13
|
+
* `enforcer-liveness.ts`; `null` means "liveness undecidable for this
|
|
14
|
+
* project" (no `src/` tree), in which case the existing file is trusted.
|
|
8
15
|
*/
|
|
9
16
|
import { existsSync } from 'node:fs';
|
|
10
17
|
import { resolve } from 'node:path';
|
|
@@ -23,37 +30,46 @@ function detectPartial(context) {
|
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
25
32
|
* Re-classify a single RedLineEntry. Returns a new entry with the
|
|
26
|
-
* `backing` field updated
|
|
27
|
-
*
|
|
33
|
+
* `backing` field updated; `enforcerRef` is preserved even when the
|
|
34
|
+
* backing is downgraded, because the dead reference is the triage
|
|
35
|
+
* information the report needs.
|
|
28
36
|
*/
|
|
29
|
-
export function classifyBacking(entry, projectRoot) {
|
|
37
|
+
export function classifyBacking(entry, projectRoot, liveEnforcers) {
|
|
38
|
+
const enforcerPath = entry.enforcerRef === null ? null : resolve(projectRoot, entry.enforcerRef);
|
|
39
|
+
const exists = enforcerPath !== null && existsSync(enforcerPath);
|
|
40
|
+
const dead = entry.enforcerRef !== null && exists && liveEnforcers !== null && !liveEnforcers.has(entry.enforcerRef);
|
|
30
41
|
if (detectPartial(entry.source.context)) {
|
|
31
42
|
return {
|
|
32
43
|
entry: { ...entry, backing: 'partial' },
|
|
33
|
-
enforcerExists:
|
|
44
|
+
enforcerExists: exists,
|
|
45
|
+
enforcerDead: dead,
|
|
34
46
|
};
|
|
35
47
|
}
|
|
36
48
|
if (entry.enforcerRef === null) {
|
|
37
|
-
return { entry, enforcerExists: false };
|
|
49
|
+
return { entry, enforcerExists: false, enforcerDead: false };
|
|
38
50
|
}
|
|
39
|
-
const
|
|
40
|
-
const exists = existsSync(enforcerPath);
|
|
51
|
+
const backed = exists && !dead;
|
|
41
52
|
return {
|
|
42
|
-
entry: { ...entry, backing:
|
|
53
|
+
entry: { ...entry, backing: backed ? 'cli-backed' : 'prose-only' },
|
|
43
54
|
enforcerExists: exists,
|
|
55
|
+
enforcerDead: dead,
|
|
44
56
|
};
|
|
45
57
|
}
|
|
46
|
-
export function classifyBackingBatch(entries, projectRoot) {
|
|
58
|
+
export function classifyBackingBatch(entries, projectRoot, liveEnforcers) {
|
|
47
59
|
const updated = [];
|
|
48
60
|
const warnings = [];
|
|
61
|
+
const dead = new Set();
|
|
49
62
|
for (const entry of entries) {
|
|
50
|
-
const
|
|
51
|
-
updated.push(
|
|
52
|
-
if (
|
|
63
|
+
const result = classifyBacking(entry, projectRoot, liveEnforcers);
|
|
64
|
+
updated.push(result.entry);
|
|
65
|
+
if (result.enforcerDead && result.entry.enforcerRef !== null) {
|
|
66
|
+
dead.add(result.entry.enforcerRef);
|
|
67
|
+
}
|
|
68
|
+
if (result.entry.backing === 'cli-backed' && !result.enforcerExists) {
|
|
53
69
|
// Defensive: should not happen because classifyBacking downgrades to
|
|
54
70
|
// prose-only, but keep the assertion in case of future drift.
|
|
55
|
-
warnings.push(`enforcer ref "${
|
|
71
|
+
warnings.push(`enforcer ref "${result.entry.enforcerRef}" missing on disk for ${result.entry.id}`);
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
|
-
return { entries: updated, warnings };
|
|
74
|
+
return { entries: updated, warnings, deadEnforcers: [...dead].sort() };
|
|
59
75
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LiveEnforcerScan {
|
|
2
|
+
/** Refs proven imported from a call site. Empty when `unknown` is true. */
|
|
3
|
+
readonly live: ReadonlySet<string>;
|
|
4
|
+
/** True when the project has no `src/` tree, so liveness is undecidable. */
|
|
5
|
+
readonly unknown: boolean;
|
|
6
|
+
readonly warnings: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Scan `projectRoot/src` for relative imports that name one of `enforcerRefs`.
|
|
10
|
+
* Returns the subset of refs with a call site outside the enforcers directory.
|
|
11
|
+
*/
|
|
12
|
+
export declare function computeLiveEnforcers(projectRoot: string, enforcerRefs: readonly string[]): LiveEnforcerScan;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforcer liveness — A9 of `docs/diagnosis-2026-09-15-peaks-loop-state.md`.
|
|
3
|
+
*
|
|
4
|
+
* `cli-backed` used to mean "the `enforcerRef` path exists on disk"
|
|
5
|
+
* (`backing-detector.ts`). It did not mean "the enforcer runs". Ten catalog
|
|
6
|
+
* enforcerRefs name a file no module imports — `lint-rd-handoff-coverage.ts`
|
|
7
|
+
* among them — so the audit reported 108/152 red lines as CLI-enforced while
|
|
8
|
+
* a quarter of those had no caller at all.
|
|
9
|
+
*
|
|
10
|
+
* This module answers the narrower, checkable question: is the enforcer
|
|
11
|
+
* module imported by some file OUTSIDE the enforcers directory? A relative
|
|
12
|
+
* import is cheap proof that a call site exists — production code cannot
|
|
13
|
+
* invoke the module without one.
|
|
14
|
+
*
|
|
15
|
+
* Deliberate limits, stated rather than hidden:
|
|
16
|
+
* - An import is not a call. A module can be imported and never invoked.
|
|
17
|
+
* This check is necessary, not sufficient: it eliminates the "no caller
|
|
18
|
+
* exists at all" class and nothing beyond it.
|
|
19
|
+
* - Imports written inside `src/…/audit/enforcers/` do not count. A dead
|
|
20
|
+
* enforcer that imports another dead enforcer must not resurrect it.
|
|
21
|
+
* - A project with no `src/` tree yields `unknown: true`, never an empty
|
|
22
|
+
* live-set: peaks-loop audits consumer projects, and "cannot tell" must
|
|
23
|
+
* not read as "dead".
|
|
24
|
+
*/
|
|
25
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
27
|
+
const SOURCE_FILE = /\.(ts|tsx|mts|cts)$/;
|
|
28
|
+
const ENFORCER_DIR_FRAGMENT = '/services/audit/enforcers/';
|
|
29
|
+
/** Matches `from './x.js'`, `import('./x.js')` and `export … from './x.js'`. */
|
|
30
|
+
const RELATIVE_JS_IMPORT = /(?:from|import)\s*\(?\s*['"](\.\.?\/[^'"]+\.js)['"]/g;
|
|
31
|
+
function toPosix(value) {
|
|
32
|
+
return value.split('\\').join('/');
|
|
33
|
+
}
|
|
34
|
+
function* walkSourceFiles(root, warnings) {
|
|
35
|
+
const stack = [root];
|
|
36
|
+
while (stack.length > 0) {
|
|
37
|
+
const dir = stack.pop();
|
|
38
|
+
if (dir === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
let dirents;
|
|
41
|
+
try {
|
|
42
|
+
dirents = readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
warnings.push(`enforcer-liveness: cannot read ${toPosix(dir)} (${String(error)})`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const dirent of dirents) {
|
|
49
|
+
const full = join(dir, dirent.name);
|
|
50
|
+
if (dirent.isDirectory()) {
|
|
51
|
+
if (dirent.name === 'node_modules' || dirent.name === 'dist' || dirent.name.startsWith('.'))
|
|
52
|
+
continue;
|
|
53
|
+
stack.push(full);
|
|
54
|
+
}
|
|
55
|
+
else if (dirent.isFile() && SOURCE_FILE.test(dirent.name)) {
|
|
56
|
+
yield full;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Scan `projectRoot/src` for relative imports that name one of `enforcerRefs`.
|
|
63
|
+
* Returns the subset of refs with a call site outside the enforcers directory.
|
|
64
|
+
*/
|
|
65
|
+
export function computeLiveEnforcers(projectRoot, enforcerRefs) {
|
|
66
|
+
const srcRoot = join(projectRoot, 'src');
|
|
67
|
+
const warnings = [];
|
|
68
|
+
const live = new Set();
|
|
69
|
+
const wanted = new Set(enforcerRefs);
|
|
70
|
+
let sawAnySource = false;
|
|
71
|
+
for (const absFile of walkSourceFiles(srcRoot, warnings)) {
|
|
72
|
+
sawAnySource = true;
|
|
73
|
+
const relFile = toPosix(relative(projectRoot, absFile));
|
|
74
|
+
// An import written inside the enforcers directory proves nothing.
|
|
75
|
+
if (relFile.includes(ENFORCER_DIR_FRAGMENT))
|
|
76
|
+
continue;
|
|
77
|
+
let source;
|
|
78
|
+
try {
|
|
79
|
+
source = readFileSync(absFile, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
warnings.push(`enforcer-liveness: cannot read ${relFile} (${String(error)})`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
RELATIVE_JS_IMPORT.lastIndex = 0;
|
|
86
|
+
let match = RELATIVE_JS_IMPORT.exec(source);
|
|
87
|
+
while (match !== null) {
|
|
88
|
+
const specifier = match[1] ?? '';
|
|
89
|
+
const resolved = toPosix(relative(projectRoot, resolve(dirname(absFile), specifier)));
|
|
90
|
+
const asTs = resolved.replace(/\.js$/, '.ts');
|
|
91
|
+
if (wanted.has(asTs))
|
|
92
|
+
live.add(asTs);
|
|
93
|
+
match = RELATIVE_JS_IMPORT.exec(source);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!sawAnySource) {
|
|
97
|
+
return { live, unknown: true, warnings };
|
|
98
|
+
}
|
|
99
|
+
return { live, unknown: false, warnings };
|
|
100
|
+
}
|
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two enforcers: catalog size must grow to ≥ 40 (the P2-a target),
|
|
5
5
|
* and the prose-only ratio must stay ≤ 7% (per spec §10.2 L2
|
|
6
|
-
* acceptance
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* acceptance). Both fire on the state the classifier produced — no
|
|
7
|
+
* extra file scan beyond what the audit already did.
|
|
8
|
+
*
|
|
9
|
+
* C6 of the 2026-09-15 diagnosis: this gate and
|
|
10
|
+
* `prose-ratio-calculator.computeProseRatio` (behind
|
|
11
|
+
* `peaks audit prose-ratio`) are the two "prose-only ratio" gates, and
|
|
12
|
+
* they used to disagree. The calculator excluded `informational` rows
|
|
13
|
+
* from its numerator; this one excluded `informational` rows from *its*
|
|
14
|
+
* numerator by a different route (red-lines-service passed it a
|
|
15
|
+
* `proseOnlyCount` that had already been filtered). Both now measure the
|
|
16
|
+
* same quantity, identically defined:
|
|
17
|
+
*
|
|
18
|
+
* numerator = rows classified `backing === 'prose-only'`, all of them
|
|
19
|
+
* denominator = every classified row (`entries.length`)
|
|
20
|
+
*
|
|
21
|
+
* They still carry different *thresholds* — 7% here, 5% as the
|
|
22
|
+
* `peaks audit prose-ratio` default — which is a policy difference, not
|
|
23
|
+
* an accounting one. Nothing redefines what is being counted.
|
|
11
24
|
*/
|
|
12
25
|
import type { LintHit } from './lint-style.js';
|
|
13
26
|
export declare const CATALOG_SIZE_TARGET = 40;
|
|
@@ -22,11 +35,10 @@ export interface CatalogProseOnlyRatio {
|
|
|
22
35
|
}
|
|
23
36
|
export declare function lintCatalogSize(actualSize: number): readonly LintHit[];
|
|
24
37
|
/**
|
|
25
|
-
* Prose-only ratio:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `.peaks/memory/2026-06-27-prose-only-catalog-followup.md`).
|
|
38
|
+
* Prose-only ratio: rows the classifier tagged `prose-only`, divided by
|
|
39
|
+
* every row the classifier produced. `catalogSize` is a slight misnomer
|
|
40
|
+
* — it is `entries.length`, the count of classified rows, not the size of
|
|
41
|
+
* the hand-maintained catalog. Same numerator and denominator as
|
|
42
|
+
* `computeProseRatio`; see the module docstring above.
|
|
31
43
|
*/
|
|
32
44
|
export declare function lintCatalogProseOnlyRatio(catalogSize: number, proseOnlyCount: number): readonly LintHit[];
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
export const CATALOG_SIZE_TARGET = 40;
|
|
2
|
-
// v2.12.1
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// fidelity-001/002, mock-placement-001, resume-detection-001,
|
|
8
|
-
// pre-rd-scan-001, design-draft-confirm-001). Bumping the target to
|
|
9
|
-
// 7% acknowledges the reform while keeping the gate active.
|
|
2
|
+
// The v2.12.1 reform left this at 7% by demoting discovered advisory
|
|
3
|
+
// rows out of the numerator. S3 of the 2026-09-15 diagnosis-remediation
|
|
4
|
+
// job removed that demotion, so the observed ratio is 66% (101/153) —
|
|
5
|
+
// the gate now fires, which is the intended outcome: the number was
|
|
6
|
+
// always this bad, it was just being reported as 0%.
|
|
10
7
|
export const PROSE_ONLY_RATIO_TARGET = 0.07;
|
|
11
8
|
function syntheticHit(catalogId, rule, matched) {
|
|
12
9
|
// No specific file to point at — return a synthetic hit against
|
|
@@ -31,12 +28,11 @@ export function lintCatalogSize(actualSize) {
|
|
|
31
28
|
return [syntheticHit('rl-catalog-total-001', 'Catalog governance: catalog size must grow to ≥ 40 (L2.3 P2-a target)', `(catalog size ${actualSize} < target ${CATALOG_SIZE_TARGET})`)];
|
|
32
29
|
}
|
|
33
30
|
/**
|
|
34
|
-
* Prose-only ratio:
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* `.peaks/memory/2026-06-27-prose-only-catalog-followup.md`).
|
|
31
|
+
* Prose-only ratio: rows the classifier tagged `prose-only`, divided by
|
|
32
|
+
* every row the classifier produced. `catalogSize` is a slight misnomer
|
|
33
|
+
* — it is `entries.length`, the count of classified rows, not the size of
|
|
34
|
+
* the hand-maintained catalog. Same numerator and denominator as
|
|
35
|
+
* `computeProseRatio`; see the module docstring above.
|
|
40
36
|
*/
|
|
41
37
|
export function lintCatalogProseOnlyRatio(catalogSize, proseOnlyCount) {
|
|
42
38
|
if (catalogSize === 0)
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
+
import type { LintHit, SkillFile } from './lint-style.js';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - md-121 : "do not hand off to QA without [tech-doc.md]" (BLOCKING)
|
|
6
|
-
* - md-127 : "do not hand off to QA without a perf-baseline" (BLOCKING)
|
|
7
|
-
* - md-162 : "100% coverage target on testable files is meaningful"
|
|
8
|
-
*
|
|
9
|
-
* Two enforcers (handoff + coverage). The handoff enforcer
|
|
10
|
-
* checks for both 'tech-doc' and 'perf-baseline' handoff
|
|
11
|
-
* markers; the coverage enforcer checks for the 100%-target
|
|
12
|
-
* phrasing + the no-coverage-padding rule.
|
|
13
|
-
*
|
|
14
|
-
* scope: peaks-rd only.
|
|
3
|
+
* peaks-rd must not hand off to QA without a reviewable RD artifact. The
|
|
4
|
+
* check reads `.peaks/_runtime/<sessionId>/rd/requests/*.md`; it does not
|
|
5
|
+
* read the SKILL.md sentence that promises one.
|
|
15
6
|
*/
|
|
16
|
-
|
|
17
|
-
export declare function lintRdHandoffContract(skill: SkillFile): ReadonlyArray<LintHit>;
|
|
7
|
+
export declare function lintRdHandoffContract(skill: SkillFile, projectRoot: string): ReadonlyArray<LintHit>;
|
|
18
8
|
export declare function lintRdCoverageDiscipline(skill: SkillFile): ReadonlyArray<LintHit>;
|
|
@@ -1,17 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* P2-b sweep 005 — peaks-rd handoff + coverage enforcers.
|
|
3
|
+
*
|
|
4
|
+
* Closes three peaks-rd discovered lines:
|
|
5
|
+
* - md-121 : "do not hand off to QA without [the RD artifact]" (BLOCKING)
|
|
6
|
+
* - md-127 : "do not hand off to QA without a perf-baseline" (BLOCKING)
|
|
7
|
+
* - md-162 : "100% coverage target on testable files is meaningful"
|
|
8
|
+
*
|
|
9
|
+
* A10 of the 2026-09-15 diagnosis — `lintRdHandoffContract` used to regex
|
|
10
|
+
* the peaks-rd SKILL.md prose for the sentence *"do not hand off to QA
|
|
11
|
+
* without this file"* and call that enforcement. It never opened a file
|
|
12
|
+
* under `.peaks/_runtime/`. The gate was verifying a sentence about the
|
|
13
|
+
* artifact instead of the artifact: the exact failure the 4.0.49 release
|
|
14
|
+
* note named, found alive inside the enforcer layer.
|
|
15
|
+
*
|
|
16
|
+
* It now reads `.peaks/_runtime/<sessionId>/rd/requests/*.md` — the
|
|
17
|
+
* artifact the sentence is about (contract:
|
|
18
|
+
* `skills/bee/peaks-rd/references/artifact-per-request.md`) — and reports
|
|
19
|
+
* when nothing is there. When no session binding can be resolved it
|
|
20
|
+
* reports nothing, matching the soft-pass convention of its sibling
|
|
21
|
+
* enforcers (`pre-rd-scan.ts`, `lint-audit-regression.ts`).
|
|
22
|
+
*
|
|
23
|
+
* `lintRdCoverageDiscipline` still reads the skill doc, because its rule
|
|
24
|
+
* *is* about what the skill doc declares. It is not a handoff gate.
|
|
25
|
+
*
|
|
26
|
+
* scope: peaks-rd only.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
29
|
+
import { join } from 'node:path';
|
|
3
30
|
const COVERAGE_TARGET = /100%\s*coverage target[^\n]*testable files/i;
|
|
4
31
|
const NO_PADDING = /must not write coverage-padding tests/i;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
32
|
+
/** The artifact whose absence makes an RD→QA handoff invalid. */
|
|
33
|
+
const RD_ARTIFACT_RELATIVE = '.peaks/_runtime';
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the bound session id from `.peaks/_runtime/session.json`. Both
|
|
36
|
+
* key spellings are accepted: the file has shipped as `peakSessionId` and
|
|
37
|
+
* as `sessionId`, and reading only one silently disables the check.
|
|
38
|
+
*/
|
|
39
|
+
function resolveSessionBinding(projectRoot) {
|
|
40
|
+
const sessionJsonPath = join(projectRoot, '.peaks', '_runtime', 'session.json');
|
|
41
|
+
if (!existsSync(sessionJsonPath)) {
|
|
42
|
+
return { sessionId: null, reason: 'no .peaks/_runtime/session.json' };
|
|
43
|
+
}
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(readFileSync(sessionJsonPath, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return { sessionId: null, reason: `session.json is not readable JSON (${String(error)})` };
|
|
50
|
+
}
|
|
51
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
52
|
+
return { sessionId: null, reason: 'session.json is not an object' };
|
|
53
|
+
}
|
|
54
|
+
const record = parsed;
|
|
55
|
+
for (const key of ['sessionId', 'peakSessionId']) {
|
|
56
|
+
const value = record[key];
|
|
57
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
58
|
+
return { sessionId: value, reason: `bound via session.json:${key}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { sessionId: null, reason: 'session.json carries no session id' };
|
|
62
|
+
}
|
|
63
|
+
/** Count non-empty `*.md` files in `dir`. A missing dir counts as zero. */
|
|
64
|
+
function countNonEmptyArtifacts(dir) {
|
|
65
|
+
let names;
|
|
66
|
+
try {
|
|
67
|
+
names = readdirSync(dir).filter((name) => name.endsWith('.md'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 0;
|
|
13
71
|
}
|
|
14
|
-
|
|
72
|
+
let count = 0;
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
try {
|
|
75
|
+
if (statSync(join(dir, name)).size > 0)
|
|
76
|
+
count += 1;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return count;
|
|
15
83
|
}
|
|
16
84
|
function findCoverageContract(lines) {
|
|
17
85
|
let target = false;
|
|
@@ -24,26 +92,27 @@ function findCoverageContract(lines) {
|
|
|
24
92
|
}
|
|
25
93
|
return { target, noPadding };
|
|
26
94
|
}
|
|
27
|
-
|
|
95
|
+
/**
|
|
96
|
+
* peaks-rd must not hand off to QA without a reviewable RD artifact. The
|
|
97
|
+
* check reads `.peaks/_runtime/<sessionId>/rd/requests/*.md`; it does not
|
|
98
|
+
* read the SKILL.md sentence that promises one.
|
|
99
|
+
*/
|
|
100
|
+
export function lintRdHandoffContract(skill, projectRoot) {
|
|
28
101
|
if (skill.name !== 'peaks-rd')
|
|
29
102
|
return [];
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
103
|
+
const binding = resolveSessionBinding(projectRoot);
|
|
104
|
+
if (binding.sessionId === null)
|
|
105
|
+
return [];
|
|
106
|
+
const requestsDir = join(projectRoot, RD_ARTIFACT_RELATIVE, binding.sessionId, 'rd', 'requests');
|
|
107
|
+
const artifactCount = countNonEmptyArtifacts(requestsDir);
|
|
108
|
+
if (artifactCount > 0)
|
|
35
109
|
return [];
|
|
36
|
-
const missing = [];
|
|
37
|
-
if (!techDoc)
|
|
38
|
-
missing.push('tech-doc handoff BLOCKING');
|
|
39
|
-
if (!perfBaseline)
|
|
40
|
-
missing.push('perf-baseline handoff BLOCKING');
|
|
41
110
|
return [{
|
|
42
111
|
catalogId: 'rl-rd-handoff-contract-001',
|
|
43
|
-
rule: 'peaks-rd
|
|
44
|
-
file:
|
|
112
|
+
rule: 'peaks-rd must not hand off to QA without a non-empty RD artifact under rd/requests/',
|
|
113
|
+
file: requestsDir,
|
|
45
114
|
line: 1,
|
|
46
|
-
matchedText: `
|
|
115
|
+
matchedText: `no non-empty .md artifact under ${requestsDir} (${binding.reason})`,
|
|
47
116
|
}];
|
|
48
117
|
}
|
|
49
118
|
export function lintRdCoverageDiscipline(skill) {
|
|
@@ -14,7 +14,15 @@ export interface SkillFile {
|
|
|
14
14
|
export declare function readSkillFiles(skillsRoot: string, names: readonly string[]): readonly SkillFile[];
|
|
15
15
|
/** Theme A — section structure. Returns lint hits (positive = rule
|
|
16
16
|
* satisfied, so a missing heading fires the lint hit; downstream
|
|
17
|
-
* audit service decides whether to WARN or pass).
|
|
17
|
+
* audit service decides whether to WARN or pass).
|
|
18
|
+
*
|
|
19
|
+
* A11 of the 2026-09-15 diagnosis: the `Hard contracts` rule used to
|
|
20
|
+
* match the *heading text* and stop there. `peaks-perf-audit/SKILL.md`
|
|
21
|
+
* carries `## Hard contracts (BLOCKING)` — the word BLOCKING is in the
|
|
22
|
+
* heading — while every bullet beneath it is unmarked prose, so the
|
|
23
|
+
* audit counted a BLOCKING red line with no contract behind it. The
|
|
24
|
+
* rule now also requires at least one marker line inside the section
|
|
25
|
+
* body, which is what makes a contract visible to the classifier. */
|
|
18
26
|
export declare function lintSectionShape(skill: SkillFile): readonly LintHit[];
|
|
19
27
|
/**
|
|
20
28
|
* ASCII wireframe section-order check (spec §5.4 line 647).
|
|
@@ -45,13 +45,36 @@ function matchedText(lines, line) {
|
|
|
45
45
|
return '';
|
|
46
46
|
return (lines[line - 1] ?? '').trim();
|
|
47
47
|
}
|
|
48
|
+
/** A red-line marker that makes a contract line machine-visible. */
|
|
49
|
+
const CONTRACT_MARKER = /\b(MANDATORY|BLOCKING|MUST NOT|RED LINE)\b/;
|
|
50
|
+
/**
|
|
51
|
+
* Collect the body of the section that starts at `headingLine` (1-based):
|
|
52
|
+
* every line up to the next `## ` heading.
|
|
53
|
+
*/
|
|
54
|
+
function sectionBody(lines, headingLine) {
|
|
55
|
+
const body = [];
|
|
56
|
+
for (let i = headingLine; i < lines.length; i += 1) {
|
|
57
|
+
if (/^##\s/.test(lines[i] ?? ''))
|
|
58
|
+
break;
|
|
59
|
+
body.push(lines[i] ?? '');
|
|
60
|
+
}
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
48
63
|
/** Theme A — section structure. Returns lint hits (positive = rule
|
|
49
64
|
* satisfied, so a missing heading fires the lint hit; downstream
|
|
50
|
-
* audit service decides whether to WARN or pass).
|
|
65
|
+
* audit service decides whether to WARN or pass).
|
|
66
|
+
*
|
|
67
|
+
* A11 of the 2026-09-15 diagnosis: the `Hard contracts` rule used to
|
|
68
|
+
* match the *heading text* and stop there. `peaks-perf-audit/SKILL.md`
|
|
69
|
+
* carries `## Hard contracts (BLOCKING)` — the word BLOCKING is in the
|
|
70
|
+
* heading — while every bullet beneath it is unmarked prose, so the
|
|
71
|
+
* audit counted a BLOCKING red line with no contract behind it. The
|
|
72
|
+
* rule now also requires at least one marker line inside the section
|
|
73
|
+
* body, which is what makes a contract visible to the classifier. */
|
|
51
74
|
export function lintSectionShape(skill) {
|
|
52
75
|
const hits = [];
|
|
53
76
|
const rules = [
|
|
54
|
-
{ id: 'rl-section-hard-contracts-001', rule: 'Hard contracts for browser/IO surface', pattern: SECTION_HARD_CONTRACTS_HEADING },
|
|
77
|
+
{ id: 'rl-section-hard-contracts-001', rule: 'Hard contracts for browser/IO surface', pattern: SECTION_HARD_CONTRACTS_HEADING, requiresBodyMarker: true },
|
|
55
78
|
{ id: 'rl-section-mandatory-artifact-001', rule: 'Mandatory per-request artifact', pattern: SECTION_MANDATORY_HEADING },
|
|
56
79
|
{ id: 'rl-section-default-runbook-001', rule: 'Default runbook pointer', pattern: SECTION_DEFAULT_RUNBOOK_HEADING },
|
|
57
80
|
{ id: 'rl-section-gate-index-001', rule: 'Gate index', pattern: SECTION_GATE_INDEX_HEADING },
|
|
@@ -67,6 +90,19 @@ export function lintSectionShape(skill) {
|
|
|
67
90
|
line: 1,
|
|
68
91
|
matchedText: '(missing section)'
|
|
69
92
|
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (r.requiresBodyMarker === true) {
|
|
96
|
+
const declared = sectionBody(skill.lines, line).some((l) => CONTRACT_MARKER.test(l));
|
|
97
|
+
if (!declared) {
|
|
98
|
+
hits.push({
|
|
99
|
+
catalogId: r.id,
|
|
100
|
+
rule: r.rule,
|
|
101
|
+
file: skill.path,
|
|
102
|
+
line,
|
|
103
|
+
matchedText: `(heading at line ${line} but no BLOCKING/MANDATORY/MUST NOT/RED LINE line inside the section)`
|
|
104
|
+
});
|
|
105
|
+
}
|
|
70
106
|
}
|
|
71
107
|
}
|
|
72
108
|
return hits;
|
|
@@ -1,33 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prose-only ratio calculator — Slice C Group G3 (v2.14.0)
|
|
2
|
+
* Prose-only ratio calculator — Slice C Group G3 (v2.14.0),
|
|
3
|
+
* corrected in S3 of the 2026-09-15 diagnosis-remediation job.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* prose-only-catalog-followup.md`), an entry counts as prose-only
|
|
7
|
-
* only when BOTH:
|
|
8
|
-
* 1. `backing === 'prose-only'`
|
|
9
|
-
* 2. `informational !== true`
|
|
5
|
+
* An entry counts as prose-only when `backing === 'prose-only'`.
|
|
6
|
+
* Full stop. There is no second condition.
|
|
10
7
|
*
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* The pre-S3 version also required `informational !== true`, on the
|
|
9
|
+
* reasoning that auto-discovered advisory SKILL.md phrases are "not
|
|
10
|
+
* actionable red lines". Whatever the merits of that reading, the effect
|
|
11
|
+
* was to move 44 of 152 rows — 29% of the catalog — out of the
|
|
12
|
+
* denominator, so the gate reported `proseOnly: 0` while the same JSON
|
|
13
|
+
* carried 44 rows with `"backing": "prose-only"`. A metric whose
|
|
14
|
+
* denominator can be redefined by the code it measures is not a metric.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* `informational` survives as a triage label — `discoveredProseOnly`
|
|
17
|
+
* below counts those rows — but it no longer moves any number that the
|
|
18
|
+
* ratio is computed from. Expect the ratio to look much worse than it
|
|
19
|
+
* did; that is this correction working.
|
|
20
|
+
*
|
|
21
|
+
* Karpathy §2 simplicity: one exported function plus a thin calculator
|
|
22
|
+
* interface; no I/O.
|
|
19
23
|
*/
|
|
20
24
|
import type { RedLineEntry } from './types.js';
|
|
21
25
|
export interface ProseRatioResult {
|
|
22
|
-
/** Total
|
|
26
|
+
/** Total entries considered (entries.length) — the denominator, always. */
|
|
23
27
|
readonly totalRedLines: number;
|
|
24
28
|
/** Count of entries with backing === 'cli-backed'. */
|
|
25
29
|
readonly cliBacked: number;
|
|
26
30
|
/** Count of entries with backing === 'partial'. */
|
|
27
31
|
readonly partial: number;
|
|
28
|
-
/** Count of entries with backing === 'prose-only'
|
|
32
|
+
/** Count of entries with backing === 'prose-only'. THE numerator. */
|
|
29
33
|
readonly proseOnly: number;
|
|
30
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Breakdown only — the subset of `proseOnly` that carries
|
|
36
|
+
* `informational: true` (auto-discovered advisory phrases with no
|
|
37
|
+
* catalog template). Included in `proseOnly`; changing it changes
|
|
38
|
+
* nothing about `proseOnly` or `ratio`.
|
|
39
|
+
*/
|
|
40
|
+
readonly discoveredProseOnly: number;
|
|
41
|
+
/** Count of entries with informational === true, whatever their backing. */
|
|
31
42
|
readonly informational: number;
|
|
32
43
|
/** proseOnly / totalRedLines. Returns 0 when totalRedLines === 0. */
|
|
33
44
|
readonly ratio: number;
|