freeharness 0.4.1 → 0.4.2
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.md +8 -7
- package/canonical/HARVEST.yaml +25 -14
- package/canonical/PROVENANCE.yaml +1 -1
- package/canonical/agents/README.md +77 -36
- package/canonical/agents/agentic-architect.md +2 -1
- package/canonical/agents/database-reviewer.md +6 -8
- package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
- package/canonical/agents/implementer.md +30 -0
- package/canonical/agents/pr-reviewer.md +9 -11
- package/canonical/agents/python-reviewer.md +7 -9
- package/canonical/agents/report-formatter.md +7 -3
- package/canonical/agents/security-reviewer.md +7 -8
- package/canonical/agents/test-writer.md +5 -2
- package/canonical/hooks/cost-prices.json +11 -9
- package/canonical/hooks/freeharness-config-guard.mjs +8072 -87
- package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
- package/canonical/hooks/freeharness-converge.mjs +78 -23
- package/canonical/hooks/freeharness-cost-tracker.mjs +1129 -218
- package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
- package/canonical/hooks/freeharness-precompact.mjs +217 -87
- package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
- package/canonical/hooks/freeharness-reload-context.mjs +197 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
- package/canonical/hooks/freeharness-sdd-context.mjs +970 -0
- package/canonical/hooks/freeharness-session-start +297 -59
- package/canonical/hooks/hooks.claude-code.json +1 -1
- package/canonical/hooks/hooks.config-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
- package/canonical/hooks/hooks.converge.claude-code.json +1 -1
- package/canonical/hooks/hooks.memory-guard.claude-code.json +3 -2
- package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
- package/canonical/openspec/schema.spec-driven.yaml +29 -8
- package/canonical/openspec/templates/follow-ups.md +3 -2
- package/canonical/openspec/templates/proposal.md +1 -1
- package/canonical/openspec/templates/tasks.md +8 -0
- package/canonical/rules/CLAUDE.md +5 -4
- package/canonical/rules/routing.md +10 -9
- package/canonical/skills/agent-evaluation/SKILL.md +17 -6
- package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
- package/canonical/skills/agentic-architecture/SKILL.md +4 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
- package/canonical/skills/code-review/SKILL.md +9 -2
- package/canonical/skills/code-review/evals/cases.yaml +1118 -27
- package/canonical/skills/git-workflow/SKILL.md +2 -0
- package/canonical/skills/local-code/SKILL.md +2 -2
- package/canonical/skills/local-code/evals/cases.yaml +1 -1
- package/canonical/skills/report-formatting/SKILL.md +10 -2
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
- package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
- package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
- package/canonical/skills/report-formatting/reference/authoring.md +25 -37
- package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
- package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
- package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
- package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
- package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
- package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +3496 -19
- package/canonical/skills/subagent-driven-development/reference/brief.md +185 -35
- package/canonical/skills/subagent-driven-development/reference/handoff.md +82 -2
- package/canonical/skills/subagent-driven-development/reference/loop.md +57 -0
- package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
- package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4771 -326
- package/dist/adapters/harness/claude-code.d.ts +19 -3
- package/dist/adapters/harness/claude-code.js +204 -39
- package/dist/adapters/harness/opencode.js +16 -2
- package/dist/adapters/harness/plugin.js +28 -17
- package/dist/adapters/memory/doctor.d.ts +174 -29
- package/dist/adapters/memory/doctor.js +758 -134
- package/dist/adapters/memory/migrate.d.ts +95 -0
- package/dist/adapters/memory/migrate.js +497 -0
- package/dist/adapters/memory/paths.d.ts +22 -0
- package/dist/adapters/memory/paths.js +85 -0
- package/dist/adapters/memory/vendored.d.ts +56 -29
- package/dist/adapters/memory/vendored.js +191 -154
- package/dist/adapters/sync/diff.js +4 -1
- package/dist/cli/cleanup-prompt.d.ts +35 -0
- package/dist/cli/cleanup-prompt.js +68 -0
- package/dist/cli/cleanup.d.ts +22 -24
- package/dist/cli/cleanup.js +182 -68
- package/dist/cli/cost-quota.d.ts +158 -0
- package/dist/cli/cost-quota.js +645 -0
- package/dist/cli/cost.d.ts +408 -46
- package/dist/cli/cost.js +1731 -57
- package/dist/cli/doctor.d.ts +52 -13
- package/dist/cli/doctor.js +193 -44
- package/dist/cli/hard-cleanup.d.ts +93 -0
- package/dist/cli/hard-cleanup.js +552 -0
- package/dist/cli/install-facts.d.ts +28 -0
- package/dist/cli/install-facts.js +66 -0
- package/dist/cli/json-key-edit.d.ts +18 -0
- package/dist/cli/json-key-edit.js +118 -0
- package/dist/cli/memory-import.d.ts +74 -0
- package/dist/cli/memory-import.js +469 -0
- package/dist/cli/plugin-validate.d.ts +12 -0
- package/dist/cli/plugin-validate.js +43 -0
- package/dist/cli/uninstall.d.ts +1 -1
- package/dist/cli/uninstall.js +2 -2
- package/dist/cli/upstream-launchers.d.ts +49 -0
- package/dist/cli/upstream-launchers.js +287 -0
- package/dist/index.js +247 -70
- package/dist/infra/fsutil.d.ts +31 -3
- package/dist/infra/fsutil.js +69 -6
- package/dist/infra/manifest.d.ts +2 -1
- package/dist/infra/manifest.js +3 -2
- package/dist/infra/prices.d.ts +18 -0
- package/dist/infra/prices.js +64 -0
- package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
- package/node_modules/@freeharness/core/dist/tiers.js +24 -2
- package/node_modules/@freeharness/core/package.json +1 -1
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
- package/node_modules/@freeharness/sdd/package.json +1 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
- package/package.json +6 -4
- package/provenance/canonical.PROVENANCE.yaml +1 -1
- package/provenance/memory.PROVENANCE.yaml +458 -6
- package/provenance/sdd.PROVENANCE.yaml +279 -2
- package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
- package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
|
@@ -14,9 +14,10 @@ export declare function normalizeRequirementName(name: string): string;
|
|
|
14
14
|
/**
|
|
15
15
|
* Case- and whitespace-insensitive fold of a requirement name. Requirement
|
|
16
16
|
* matching itself is case-sensitive (normalizeRequirementName); this fold
|
|
17
|
-
* exists only for typo detection - near-miss REMOVED
|
|
18
|
-
* RENAMED+REMOVED cross-section conflict - where two
|
|
19
|
-
* only in case or interior whitespace mean a mistake,
|
|
17
|
+
* exists only for typo detection - near-miss REMOVED, ADDED and RENAMED
|
|
18
|
+
* headers and the RENAMED+REMOVED cross-section conflict - where two
|
|
19
|
+
* spellings that differ only in case or interior whitespace mean a mistake,
|
|
20
|
+
* never two requirements.
|
|
20
21
|
*/
|
|
21
22
|
export declare function foldRequirementName(name: string): string;
|
|
22
23
|
/**
|
|
@@ -33,14 +34,36 @@ export interface SkippedHeader {
|
|
|
33
34
|
section: string;
|
|
34
35
|
line: number;
|
|
35
36
|
}
|
|
37
|
+
/** A `TO:` line in RENAMED with no pending `FROM:` - not left to silently pair with a later, unrelated `FROM:`. */
|
|
38
|
+
export interface UnpairedRename {
|
|
39
|
+
side: 'TO';
|
|
40
|
+
name: string;
|
|
41
|
+
line: number;
|
|
42
|
+
}
|
|
43
|
+
/** A canonical `### Requirement:` block outside every delta section (`## Notes`, above the first `## ` header, ...). */
|
|
44
|
+
export interface OrphanedRequirement {
|
|
45
|
+
name: string;
|
|
46
|
+
section: string | null;
|
|
47
|
+
line: number;
|
|
48
|
+
}
|
|
49
|
+
/** Delta text archive never carries into the main spec; `text` lets a warning quote it. */
|
|
50
|
+
export interface UnappliedContent {
|
|
51
|
+
section: string | null;
|
|
52
|
+
line: number;
|
|
53
|
+
text: string;
|
|
54
|
+
}
|
|
36
55
|
export interface DeltaPlan {
|
|
37
56
|
added: RequirementBlock[];
|
|
38
57
|
modified: RequirementBlock[];
|
|
39
58
|
removed: string[];
|
|
59
|
+
removedBlocks: RequirementBlock[];
|
|
40
60
|
renamed: Array<{
|
|
41
61
|
from: string;
|
|
42
62
|
to: string;
|
|
43
63
|
}>;
|
|
64
|
+
unpairedRenames: UnpairedRename[];
|
|
65
|
+
orphanedRequirements: OrphanedRequirement[];
|
|
66
|
+
unappliedContent: UnappliedContent[];
|
|
44
67
|
skippedHeaders: SkippedHeader[];
|
|
45
68
|
sectionPresence: {
|
|
46
69
|
added: boolean;
|
|
@@ -53,13 +76,30 @@ export interface DeltaPlan {
|
|
|
53
76
|
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
54
77
|
*/
|
|
55
78
|
export declare function parseDeltaSpec(content: string): DeltaPlan;
|
|
79
|
+
export declare function describeUnappliedContent(content: UnappliedContent, sentenceStart?: boolean): string;
|
|
80
|
+
/** Standalone entry for callers holding raw file text rather than a plan. */
|
|
81
|
+
export declare function findUnappliedContent(content: string): UnappliedContent[];
|
|
82
|
+
/** Both directions of the scenario-name comparison, plus the two totals. */
|
|
83
|
+
export interface ScenarioNameDiff {
|
|
84
|
+
/** Names the current block has that the incoming block does not cover. */
|
|
85
|
+
missing: string[];
|
|
86
|
+
/** Names the incoming block introduces that the current block does not have. */
|
|
87
|
+
added: string[];
|
|
88
|
+
/** Level-4 headers in the current block. */
|
|
89
|
+
currentCount: number;
|
|
90
|
+
/** Level-4 headers in the incoming block. */
|
|
91
|
+
incomingCount: number;
|
|
92
|
+
}
|
|
56
93
|
/**
|
|
57
|
-
*
|
|
58
|
-
* (MODIFIED)
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* about what counts as a dropped scenario.
|
|
94
|
+
* Compare the scenario names of a current requirement block and an incoming
|
|
95
|
+
* (MODIFIED) one, in both directions. `missing` is the loss the guard exists
|
|
96
|
+
* to catch; `added` and the two counts are reported alongside it, since a
|
|
97
|
+
* block that omits two names and introduces two reads as a rename while one
|
|
98
|
+
* that introduces none reads as a truncation - neither is proof, so this
|
|
99
|
+
* decides nothing and only says what the two blocks contain.
|
|
64
100
|
*/
|
|
101
|
+
export declare function diffScenarioNames(current: RequirementBlock, incoming: RequirementBlock): ScenarioNameDiff;
|
|
102
|
+
/** Scenario names the current block has that the incoming block drops - the `missing` half of diffScenarioNames. */
|
|
65
103
|
export declare function findMissingCurrentScenarios(current: RequirementBlock, incoming: RequirementBlock): string[];
|
|
104
|
+
/** The one sentence archive and validate both append when the scenario-loss guard fires. */
|
|
105
|
+
export declare function describeScenarioBalance(diff: ScenarioNameDiff): string;
|
package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { buildCodeFenceMask, SCENARIO_HEADER } from './requirement-text.js';
|
|
2
2
|
export function normalizeRequirementName(name) {
|
|
3
|
-
|
|
3
|
+
// A trailing ATX closing run of `#`s is not part of the name (`### Foo ###`
|
|
4
|
+
// renders as `Foo`); only a run preceded by a space/tab closes the heading,
|
|
5
|
+
// so `C#` keeps its `#`.
|
|
6
|
+
return name.replace(/[ \t]+#+[ \t]*$/, '').trim();
|
|
4
7
|
}
|
|
5
8
|
/**
|
|
6
9
|
* Case- and whitespace-insensitive fold of a requirement name. Requirement
|
|
7
10
|
* matching itself is case-sensitive (normalizeRequirementName); this fold
|
|
8
|
-
* exists only for typo detection - near-miss REMOVED
|
|
9
|
-
* RENAMED+REMOVED cross-section conflict - where two
|
|
10
|
-
* only in case or interior whitespace mean a mistake,
|
|
11
|
+
* exists only for typo detection - near-miss REMOVED, ADDED and RENAMED
|
|
12
|
+
* headers and the RENAMED+REMOVED cross-section conflict - where two
|
|
13
|
+
* spellings that differ only in case or interior whitespace mean a mistake,
|
|
14
|
+
* never two requirements.
|
|
11
15
|
*/
|
|
12
16
|
export function foldRequirementName(name) {
|
|
13
17
|
return normalizeRequirementName(name).toLowerCase().replace(/\s+/g, ' ');
|
|
@@ -115,15 +119,22 @@ export function parseDeltaSpec(content) {
|
|
|
115
119
|
sink: skippedHeaders,
|
|
116
120
|
}));
|
|
117
121
|
const removedNames = removedLookup.bodies.flatMap((body) => parseRemovedNames(body));
|
|
122
|
+
const removedBlocks = removedLookup.bodies.flatMap((body) => parseRequirementBlocksFromSection(body));
|
|
118
123
|
// Pairs are read per section, so a FROM in one copy of the header can never
|
|
119
124
|
// pair with a TO written under a different copy.
|
|
120
|
-
const
|
|
125
|
+
const unpairedRenames = [];
|
|
126
|
+
const renamedPairs = renamedLookup.bodies.flatMap((body) => parseRenamedPairs(body, { section: renamedLookup.title, sink: skippedHeaders }, unpairedRenames));
|
|
127
|
+
unpairedRenames.sort((a, b) => a.line - b.line);
|
|
121
128
|
skippedHeaders.sort((a, b) => a.line - b.line);
|
|
122
129
|
return {
|
|
123
130
|
added,
|
|
124
131
|
modified,
|
|
125
132
|
removed: removedNames,
|
|
133
|
+
removedBlocks,
|
|
126
134
|
renamed: renamedPairs,
|
|
135
|
+
unpairedRenames,
|
|
136
|
+
orphanedRequirements: findOrphanedRequirements(lines, fenceMask),
|
|
137
|
+
unappliedContent: collectUnappliedContent(lines, fenceMask),
|
|
127
138
|
skippedHeaders,
|
|
128
139
|
sectionPresence: {
|
|
129
140
|
added: addedLookup.found,
|
|
@@ -133,6 +144,94 @@ export function parseDeltaSpec(content) {
|
|
|
133
144
|
},
|
|
134
145
|
};
|
|
135
146
|
}
|
|
147
|
+
/** The four section titles the delta reader acts on, folded like `getSectionsCaseInsensitive` folds them. */
|
|
148
|
+
const DELTA_SECTION_TITLES = new Set(['ADDED Requirements', 'MODIFIED Requirements', 'REMOVED Requirements', 'RENAMED Requirements'].map((title) => title.toLowerCase()));
|
|
149
|
+
/** Every canonical `### Requirement:` header outside a delta section, in document order. */
|
|
150
|
+
function findOrphanedRequirements(lines, fenceMask) {
|
|
151
|
+
const orphans = [];
|
|
152
|
+
let section = null;
|
|
153
|
+
for (let i = 0; i < lines.length; i++) {
|
|
154
|
+
if (fenceMask[i])
|
|
155
|
+
continue;
|
|
156
|
+
// Same `## ` test splitTopLevelSections uses, so both agree on sections.
|
|
157
|
+
const sectionMatch = lines[i].match(/^(##)\s+(.+)$/);
|
|
158
|
+
if (sectionMatch) {
|
|
159
|
+
section = sectionMatch[2].trim();
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (section !== null && DELTA_SECTION_TITLES.has(section.toLowerCase()))
|
|
163
|
+
continue;
|
|
164
|
+
const header = lines[i].match(REQUIREMENT_HEADER_REGEX);
|
|
165
|
+
if (header) {
|
|
166
|
+
orphans.push({ name: normalizeRequirementName(header[1]), section, line: i + 1 });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return orphans;
|
|
170
|
+
}
|
|
171
|
+
/** Quoted text is cut here so one long paragraph cannot flood a warning line. */
|
|
172
|
+
const MAX_QUOTED_TEXT = 100;
|
|
173
|
+
export function describeUnappliedContent(content, sentenceStart = false) {
|
|
174
|
+
const where = content.section === null
|
|
175
|
+
? 'text above the first "## " section'
|
|
176
|
+
: DELTA_SECTION_TITLES.has(content.section.toLowerCase())
|
|
177
|
+
? `text before the first requirement in "## ${content.section}"`
|
|
178
|
+
: `section "## ${content.section}"`;
|
|
179
|
+
const quoted = content.text.length > MAX_QUOTED_TEXT ? `${content.text.slice(0, MAX_QUOTED_TEXT)}...` : content.text;
|
|
180
|
+
const text = `${where} ("${quoted}")`;
|
|
181
|
+
return sentenceStart ? text.charAt(0).toUpperCase() + text.slice(1) : text;
|
|
182
|
+
}
|
|
183
|
+
/** Standalone entry for callers holding raw file text rather than a plan. */
|
|
184
|
+
export function findUnappliedContent(content) {
|
|
185
|
+
const lines = normalizeLineEndings(content).split('\n');
|
|
186
|
+
return collectUnappliedContent(lines, buildCodeFenceMask(lines));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* HTML comments, `# ` titles, Purpose (warned about separately), REMOVED and
|
|
190
|
+
* RENAMED bodies, and stray `###` headers before the first requirement (INFO
|
|
191
|
+
* since #498) are not reported.
|
|
192
|
+
*/
|
|
193
|
+
function collectUnappliedContent(lines, fenceMask) {
|
|
194
|
+
// Comments only decide whether a line holds text; sections come from the
|
|
195
|
+
// merge's own split, and fenced lines keep their text so a `<!--` sample
|
|
196
|
+
// never hides what follows the fence.
|
|
197
|
+
const comments = lines
|
|
198
|
+
.map((line, i) => (fenceMask[i] ? '' : line))
|
|
199
|
+
.join('\n')
|
|
200
|
+
.replace(/<!--[\s\S]*?(?:--!?>|$)/g, (m) => m.replace(/[^\n]/g, ' '))
|
|
201
|
+
.split('\n');
|
|
202
|
+
const masked = lines.map((line, i) => (fenceMask[i] ? line : comments[i]));
|
|
203
|
+
const sections = splitTopLevelSections(lines, fenceMask);
|
|
204
|
+
const firstText = (from, to, skip) => {
|
|
205
|
+
for (let i = from; i < to; i++)
|
|
206
|
+
if (masked[i].trim() && !skip?.test(masked[i]))
|
|
207
|
+
return i;
|
|
208
|
+
return -1;
|
|
209
|
+
};
|
|
210
|
+
const found = [];
|
|
211
|
+
const preamble = firstText(0, sections.length ? sections[0].body.bodyStartLine - 2 : lines.length, /^#\s/);
|
|
212
|
+
if (preamble !== -1)
|
|
213
|
+
found.push({ section: null, line: preamble + 1, text: masked[preamble].trim() });
|
|
214
|
+
for (const { title, body } of sections) {
|
|
215
|
+
const folded = title.toLowerCase();
|
|
216
|
+
const start = body.bodyStartLine - 1;
|
|
217
|
+
if (folded === 'added requirements' || folded === 'modified requirements') {
|
|
218
|
+
const block = body.lines.findIndex((l, i) => !body.fenceMask[i] && REQUIREMENT_HEADER_REGEX.test(l));
|
|
219
|
+
const first = firstText(start, start + (block === -1 ? body.lines.length : block), /^###\s/);
|
|
220
|
+
if (first !== -1)
|
|
221
|
+
found.push({ section: title, line: first + 1, text: masked[first].trim() });
|
|
222
|
+
}
|
|
223
|
+
else if (!DELTA_SECTION_TITLES.has(folded)) {
|
|
224
|
+
// The Purpose carry-over reads past commented-out headers, so only a
|
|
225
|
+
// `## Purpose` outside a comment is the one archive handles.
|
|
226
|
+
if (folded === 'purpose' && masked[start - 1].trim())
|
|
227
|
+
continue;
|
|
228
|
+
const first = firstText(start, start + body.lines.length);
|
|
229
|
+
if (first !== -1)
|
|
230
|
+
found.push({ section: title, line: start, text: masked[first].trim() });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return found;
|
|
234
|
+
}
|
|
136
235
|
/**
|
|
137
236
|
* Every `## ` section of a delta file, as a list rather than a title-keyed
|
|
138
237
|
* record.
|
|
@@ -276,7 +375,7 @@ function parseRemovedNames(sectionBody) {
|
|
|
276
375
|
* or `+` into a silent no-op - archive reports success while the rename never
|
|
277
376
|
* happens.
|
|
278
377
|
*/
|
|
279
|
-
function parseRenamedPairs(sectionBody, skipped) {
|
|
378
|
+
function parseRenamedPairs(sectionBody, skipped, unpaired) {
|
|
280
379
|
const { lines, fenceMask, bodyStartLine } = sectionBody;
|
|
281
380
|
if (lines.length === 0)
|
|
282
381
|
return [];
|
|
@@ -305,47 +404,76 @@ function parseRenamedPairs(sectionBody, skipped) {
|
|
|
305
404
|
current.fromLine = i;
|
|
306
405
|
}
|
|
307
406
|
else if (toMatch) {
|
|
308
|
-
|
|
309
|
-
if (current.from
|
|
310
|
-
|
|
311
|
-
|
|
407
|
+
const to = normalizeRequirementName(toMatch[1]);
|
|
408
|
+
if (current.from === undefined) {
|
|
409
|
+
// No pending FROM: used to linger in `current.to` and silently pair
|
|
410
|
+
// with whichever FROM arrived next (#1806) - reported instead.
|
|
411
|
+
unpaired?.push({ side: 'TO', name: to, line: bodyStartLine + i });
|
|
412
|
+
continue;
|
|
312
413
|
}
|
|
414
|
+
pairs.push({ from: current.from, to });
|
|
415
|
+
current = {};
|
|
313
416
|
}
|
|
314
417
|
}
|
|
315
418
|
flushUnpairedFrom();
|
|
316
419
|
return pairs;
|
|
317
420
|
}
|
|
318
421
|
/**
|
|
319
|
-
*
|
|
320
|
-
* (MODIFIED)
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
* about what counts as a dropped scenario.
|
|
422
|
+
* Compare the scenario names of a current requirement block and an incoming
|
|
423
|
+
* (MODIFIED) one, in both directions. `missing` is the loss the guard exists
|
|
424
|
+
* to catch; `added` and the two counts are reported alongside it, since a
|
|
425
|
+
* block that omits two names and introduces two reads as a rename while one
|
|
426
|
+
* that introduces none reads as a truncation - neither is proof, so this
|
|
427
|
+
* decides nothing and only says what the two blocks contain.
|
|
326
428
|
*/
|
|
327
|
-
export function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
//
|
|
331
|
-
// (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const name
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
missing.push(name);
|
|
429
|
+
export function diffScenarioNames(current, incoming) {
|
|
430
|
+
const currentNames = parseScenarioBlocks(current.raw).map((scenario) => scenario.name);
|
|
431
|
+
const incomingNames = parseScenarioBlocks(incoming.raw).map((scenario) => scenario.name);
|
|
432
|
+
// Multiplicity-aware: a name present N times on one side and M times on the
|
|
433
|
+
// other leaves max(0, N - M) instances unmatched. Set membership would treat
|
|
434
|
+
// N>M as fully covered and let archive silently drop duplicates (residual
|
|
435
|
+
// #1246 / duplicate-scenario-name blind spot).
|
|
436
|
+
const unmatched = (names, against) => {
|
|
437
|
+
const remaining = new Map();
|
|
438
|
+
for (const name of against)
|
|
439
|
+
remaining.set(name, (remaining.get(name) ?? 0) + 1);
|
|
440
|
+
const out = [];
|
|
441
|
+
for (const name of names) {
|
|
442
|
+
const left = remaining.get(name) ?? 0;
|
|
443
|
+
if (left > 0)
|
|
444
|
+
remaining.set(name, left - 1);
|
|
445
|
+
else
|
|
446
|
+
out.push(name);
|
|
346
447
|
}
|
|
448
|
+
return out;
|
|
449
|
+
};
|
|
450
|
+
return {
|
|
451
|
+
missing: unmatched(currentNames, incomingNames),
|
|
452
|
+
added: unmatched(incomingNames, currentNames),
|
|
453
|
+
currentCount: currentNames.length,
|
|
454
|
+
incomingCount: incomingNames.length,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/** Scenario names the current block has that the incoming block drops - the `missing` half of diffScenarioNames. */
|
|
458
|
+
export function findMissingCurrentScenarios(current, incoming) {
|
|
459
|
+
return diffScenarioNames(current, incoming).missing;
|
|
460
|
+
}
|
|
461
|
+
/** At most this many added names are listed before the rest are counted. */
|
|
462
|
+
const MAX_LISTED_ADDED_SCENARIOS = 3;
|
|
463
|
+
/** The one sentence archive and validate both append when the scenario-loss guard fires. */
|
|
464
|
+
export function describeScenarioBalance(diff) {
|
|
465
|
+
const count = (value) => `${value} ${value === 1 ? 'scenario' : 'scenarios'}`;
|
|
466
|
+
const scale = `The modified block has ${count(diff.incomingCount)}; the current spec has ${count(diff.currentCount)}.`;
|
|
467
|
+
if (diff.added.length === 0) {
|
|
468
|
+
return `${scale} It adds none.`;
|
|
347
469
|
}
|
|
348
|
-
|
|
470
|
+
const listed = diff.added
|
|
471
|
+
.slice(0, MAX_LISTED_ADDED_SCENARIOS)
|
|
472
|
+
.map((name) => `"${name}"`)
|
|
473
|
+
.join(', ');
|
|
474
|
+
const rest = diff.added.length - MAX_LISTED_ADDED_SCENARIOS;
|
|
475
|
+
const names = rest > 0 ? `${listed} and ${rest} more` : listed;
|
|
476
|
+
return `${scale} It adds ${count(diff.added.length)} not in the current spec: ${names}.`;
|
|
349
477
|
}
|
|
350
478
|
/**
|
|
351
479
|
* Any non-fenced level-4 header on the given (masked) line. Reuses the spec
|
package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts
CHANGED
|
@@ -15,8 +15,26 @@ export { buildCodeFenceMask } from './code-fence.js';
|
|
|
15
15
|
* as a scenario, so the delta counter must too (parity). The delta/loss path
|
|
16
16
|
* reuses this exact constant via `scenarioHeaderAt` in requirement-blocks.ts;
|
|
17
17
|
* keep both paths on it rather than reintroducing a separate `Scenario:` regex.
|
|
18
|
+
* A header alone is not yet a scenario on either path: see hasScenarioBody.
|
|
18
19
|
*/
|
|
19
20
|
export declare const SCENARIO_HEADER: RegExp;
|
|
21
|
+
/**
|
|
22
|
+
* EARS scenario title: `Scenario: WHEN <event> THE SYSTEM SHALL <behavior>`
|
|
23
|
+
* (also WHILE/WHERE/IF ... SHALL). FreeHarness addition (V4-P4): an EARS
|
|
24
|
+
* scenario carries its whole behavior in the header and may have no body -
|
|
25
|
+
* both the spec path (`MarkdownParser.parseScenarios`) and the delta counter
|
|
26
|
+
* below count it as a real scenario, matched case-insensitively against the
|
|
27
|
+
* text after `#### `.
|
|
28
|
+
*/
|
|
29
|
+
export declare const EARS_SCENARIO_TITLE: RegExp;
|
|
30
|
+
/**
|
|
31
|
+
* Whether a scenario's body has content. The spec path
|
|
32
|
+
* (`MarkdownParser.parseScenarios`) drops a scenario whose body is empty, so
|
|
33
|
+
* the delta counter must too (parity): otherwise `validate` accepts a
|
|
34
|
+
* requirement whose only scenario is a bare header, and archive rejects it
|
|
35
|
+
* when it validates the rebuilt spec.
|
|
36
|
+
*/
|
|
37
|
+
export declare function hasScenarioBody(body: string): boolean;
|
|
20
38
|
/**
|
|
21
39
|
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
22
40
|
* as whole words so the change-delta reader and the schema-based reader accept
|
|
@@ -48,8 +66,11 @@ export declare function extractRequirementBody(bodyLines: string[]): string;
|
|
|
48
66
|
*/
|
|
49
67
|
export declare function extractRequirementText(headerTitle: string, bodyLines: string[]): string;
|
|
50
68
|
/**
|
|
51
|
-
* Count the real scenarios in a requirement block: `#### ` headers on
|
|
52
|
-
* lines
|
|
53
|
-
*
|
|
69
|
+
* Count the real scenarios in a requirement block: `#### ` headers on
|
|
70
|
+
* non-fenced lines whose body has content, or whose header is EARS-shaped. A
|
|
71
|
+
* `#### Scenario:` that lives inside a fenced example is not a real scenario
|
|
72
|
+
* and is not counted, and neither is a plain header with nothing under it.
|
|
54
73
|
*/
|
|
55
74
|
export declare function countScenarios(bodyLines: string[]): number;
|
|
75
|
+
/** Count the `#### ` headers in a requirement block that do not count as a scenario. */
|
|
76
|
+
export declare function countEmptyScenarios(bodyLines: string[]): number;
|
package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js
CHANGED
|
@@ -22,8 +22,30 @@ const HEADER_LINE = /^#{1,6}\s/;
|
|
|
22
22
|
* as a scenario, so the delta counter must too (parity). The delta/loss path
|
|
23
23
|
* reuses this exact constant via `scenarioHeaderAt` in requirement-blocks.ts;
|
|
24
24
|
* keep both paths on it rather than reintroducing a separate `Scenario:` regex.
|
|
25
|
+
* A header alone is not yet a scenario on either path: see hasScenarioBody.
|
|
25
26
|
*/
|
|
26
27
|
export const SCENARIO_HEADER = /^####\s+/;
|
|
28
|
+
/** A header at scenario level or above (`#` to `####`): where a scenario body ends. */
|
|
29
|
+
const SCENARIO_BODY_END = /^#{1,4}\s/;
|
|
30
|
+
/**
|
|
31
|
+
* EARS scenario title: `Scenario: WHEN <event> THE SYSTEM SHALL <behavior>`
|
|
32
|
+
* (also WHILE/WHERE/IF ... SHALL). FreeHarness addition (V4-P4): an EARS
|
|
33
|
+
* scenario carries its whole behavior in the header and may have no body -
|
|
34
|
+
* both the spec path (`MarkdownParser.parseScenarios`) and the delta counter
|
|
35
|
+
* below count it as a real scenario, matched case-insensitively against the
|
|
36
|
+
* text after `#### `.
|
|
37
|
+
*/
|
|
38
|
+
export const EARS_SCENARIO_TITLE = /^Scenario:\s*(?:WHEN|WHILE|WHERE|IF)\b[\s\S]*\b(?:SHALL|MUST)\b/i;
|
|
39
|
+
/**
|
|
40
|
+
* Whether a scenario's body has content. The spec path
|
|
41
|
+
* (`MarkdownParser.parseScenarios`) drops a scenario whose body is empty, so
|
|
42
|
+
* the delta counter must too (parity): otherwise `validate` accepts a
|
|
43
|
+
* requirement whose only scenario is a bare header, and archive rejects it
|
|
44
|
+
* when it validates the rebuilt spec.
|
|
45
|
+
*/
|
|
46
|
+
export function hasScenarioBody(body) {
|
|
47
|
+
return body.trim().length > 0;
|
|
48
|
+
}
|
|
27
49
|
/**
|
|
28
50
|
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
29
51
|
* as whole words so the change-delta reader and the schema-based reader accept
|
|
@@ -81,18 +103,43 @@ export function extractRequirementText(headerTitle, bodyLines) {
|
|
|
81
103
|
return extractRequirementBody(bodyLines) || headerTitle.trim();
|
|
82
104
|
}
|
|
83
105
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
|
|
106
|
+
* Whether a scenario counts as real: a non-empty body, or an EARS-shaped
|
|
107
|
+
* header, which carries its whole behavior in the title and needs none.
|
|
108
|
+
*/
|
|
109
|
+
function isCountedScenario(scenario) {
|
|
110
|
+
return hasScenarioBody(scenario.body) || EARS_SCENARIO_TITLE.test(scenario.header);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Count the real scenarios in a requirement block: `#### ` headers on
|
|
114
|
+
* non-fenced lines whose body has content, or whose header is EARS-shaped. A
|
|
115
|
+
* `#### Scenario:` that lives inside a fenced example is not a real scenario
|
|
116
|
+
* and is not counted, and neither is a plain header with nothing under it.
|
|
87
117
|
*/
|
|
88
118
|
export function countScenarios(bodyLines) {
|
|
119
|
+
return readScenarios(bodyLines).filter(isCountedScenario).length;
|
|
120
|
+
}
|
|
121
|
+
/** Count the `#### ` headers in a requirement block that do not count as a scenario. */
|
|
122
|
+
export function countEmptyScenarios(bodyLines) {
|
|
123
|
+
return readScenarios(bodyLines).filter((scenario) => !isCountedScenario(scenario)).length;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Each scenario in a requirement block, as its header text and body. A body
|
|
127
|
+
* runs to the next non-fenced header of level 4 or above, the boundary the
|
|
128
|
+
* spec path uses, so a fenced block or a deeper `#####` header is part of it.
|
|
129
|
+
*/
|
|
130
|
+
function readScenarios(bodyLines) {
|
|
89
131
|
const mask = buildCodeFenceMask(bodyLines);
|
|
90
|
-
|
|
132
|
+
const scenarios = [];
|
|
91
133
|
for (let i = 0; i < bodyLines.length; i++) {
|
|
92
|
-
if (mask[i])
|
|
134
|
+
if (mask[i] || !SCENARIO_HEADER.test(bodyLines[i]))
|
|
93
135
|
continue;
|
|
94
|
-
|
|
95
|
-
|
|
136
|
+
let end = i + 1;
|
|
137
|
+
while (end < bodyLines.length && (mask[end] || !SCENARIO_BODY_END.test(bodyLines[end])))
|
|
138
|
+
end++;
|
|
139
|
+
scenarios.push({
|
|
140
|
+
header: bodyLines[i].replace(SCENARIO_HEADER, ''),
|
|
141
|
+
body: bodyLines.slice(i + 1, end).join('\n'),
|
|
142
|
+
});
|
|
96
143
|
}
|
|
97
|
-
return
|
|
144
|
+
return scenarios;
|
|
98
145
|
}
|
package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildCodeFenceMask } from './code-fence.js';
|
|
2
|
+
import { normalizeRequirementName } from './requirement-blocks.js';
|
|
2
3
|
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
|
|
3
4
|
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
|
|
4
5
|
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
@@ -53,7 +54,9 @@ export function findMainSpecStructureIssues(content) {
|
|
|
53
54
|
});
|
|
54
55
|
continue;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
// The same name every other reader uses, so a closed heading
|
|
58
|
+
// (`### Requirement: Foo ###`) duplicates `### Requirement: Foo`.
|
|
59
|
+
const requirementName = normalizeRequirementName(requirementMatch[1]);
|
|
57
60
|
const previousLine = requirementLines.get(requirementName);
|
|
58
61
|
if (previousLine !== undefined) {
|
|
59
62
|
issues.push({
|
package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { buildCodeFenceMask } from './parsers/code-fence.js';
|
|
2
2
|
import { PURPOSE_PLACEHOLDER_PREFIX, PURPOSE_PLACEHOLDER_SUFFIX } from './constants.js';
|
|
3
3
|
/**
|
|
4
|
-
* A `TBD` or `TODO` opening the Purpose.
|
|
5
|
-
* word
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* A `TBD` or `TODO` opening the Purpose. `WORD_END` keeps the marker off a
|
|
5
|
+
* longer word merely starting with those letters ("TBDs"), while allowing the
|
|
6
|
+
* punctuation a marker is usually written with (`TODO:`, `TBD -`); it rejects
|
|
7
|
+
* any letter/digit/combining mark, not only ASCII, since prose is not always
|
|
8
|
+
* Latin script.
|
|
9
|
+
*
|
|
10
|
+
* Case separates the marker from the word: `TODO` shouted in capitals is the
|
|
11
|
+
* marker whatever follows it, so `TODO write this later` is still unwritten.
|
|
12
|
+
* Any other case is a marker only when punctuation or the line end follows,
|
|
13
|
+
* because `todo` opens ordinary Spanish/Portuguese sentences ("Todo el...",
|
|
14
|
+
* "Todo o...") that are not placeholders - while `todo - write this later`
|
|
15
|
+
* and `tbd.` still report.
|
|
11
16
|
*/
|
|
12
|
-
const
|
|
17
|
+
const WORD_END = '(?![\\p{L}\\p{N}\\p{M}_])';
|
|
18
|
+
const MARKER_PUNCTUATION = '(?=[ \\t]*(?:$|\\n|[:\\-–—.,;()\\[\\]{}]))';
|
|
19
|
+
/** `TBD`/`TODO` in capitals: the marker, whatever follows it. */
|
|
20
|
+
const LEADING_MARKER_SHOUTED = new RegExp(`^(?:TBD|TODO)${WORD_END}`, 'u');
|
|
21
|
+
/** Any other case: a marker only when punctuation or the line end says so. */
|
|
22
|
+
const LEADING_MARKER_PUNCTUATED = new RegExp(`^(?:TBD|TODO)${WORD_END}${MARKER_PUNCTUATION}`, 'iu');
|
|
13
23
|
const PURPOSE_HEADER = /^ {0,3}##(?!#)[ \t]+Purpose[ \t]*$/i;
|
|
14
24
|
const TOP_LEVEL_HEADER = /^ {0,3}#{1,2}(?!#)[ \t]+/;
|
|
15
25
|
/**
|
|
@@ -60,7 +70,7 @@ export function findPurposePlaceholderIssue(overview, content) {
|
|
|
60
70
|
// that is nothing but a fenced block reduces to the same empty text here, and
|
|
61
71
|
// is left to the brevity and empty-Purpose rules for the same reason.
|
|
62
72
|
const prose = unfencedLines(overview).join('\n').trim();
|
|
63
|
-
const leading =
|
|
73
|
+
const leading = LEADING_MARKER_SHOUTED.test(prose) || LEADING_MARKER_PUNCTUATED.test(prose);
|
|
64
74
|
if (!leading && generatedPlaceholderPrefixIndex(prose) === undefined)
|
|
65
75
|
return null;
|
|
66
76
|
// Which rule matched decides where the placeholder is, so the locator is told.
|
|
@@ -37,3 +37,13 @@ export declare function discoverSpecFiles(specsRoot: string): Promise<Discovered
|
|
|
37
37
|
* decide.
|
|
38
38
|
*/
|
|
39
39
|
export declare function hasAnyFileUnder(dirPath: string): Promise<boolean>;
|
|
40
|
+
export interface UnreadDeltaFile {
|
|
41
|
+
/** File path relative to the specs root, forward-slash separated. */
|
|
42
|
+
path: string;
|
|
43
|
+
/** The spec.md the merge path reads for it, relative to the specs root. */
|
|
44
|
+
expected: string;
|
|
45
|
+
}
|
|
46
|
+
/** Async driver of walkUnreadDeltaFiles. */
|
|
47
|
+
export declare function findUnreadDeltaFiles(specsRoot: string): Promise<UnreadDeltaFile[]>;
|
|
48
|
+
/** Sync driver of walkUnreadDeltaFiles, for the instructions shim. */
|
|
49
|
+
export declare function findUnreadDeltaFilesSync(specsRoot: string): UnreadDeltaFile[];
|