perttool 0.10.6 → 0.11.1
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 +56 -1
- package/README.md +273 -609
- package/dist/application/cli-facade.d.ts +4 -10
- package/dist/application/cli-facade.d.ts.map +1 -1
- package/dist/application/cli-facade.js +1 -1
- package/dist/application/cli-facade.js.map +1 -1
- package/dist/application/contract10-candidate.d.ts +33 -0
- package/dist/application/contract10-candidate.d.ts.map +1 -0
- package/dist/application/contract10-candidate.js +38 -0
- package/dist/application/contract10-candidate.js.map +1 -0
- package/dist/application/contract10-planning-history.d.ts +4 -0
- package/dist/application/contract10-planning-history.d.ts.map +1 -0
- package/dist/application/contract10-planning-history.js +28 -0
- package/dist/application/contract10-planning-history.js.map +1 -0
- package/dist/application/contract10-planning.d.ts +61 -0
- package/dist/application/contract10-planning.d.ts.map +1 -0
- package/dist/application/contract10-planning.js +165 -0
- package/dist/application/contract10-planning.js.map +1 -0
- package/dist/application/contract10-runtime.d.ts +185 -0
- package/dist/application/contract10-runtime.d.ts.map +1 -0
- package/dist/application/contract10-runtime.js +236 -0
- package/dist/application/contract10-runtime.js.map +1 -0
- package/dist/application/contract8-milestone-acceptance.d.ts +6 -22
- package/dist/application/contract8-milestone-acceptance.d.ts.map +1 -1
- package/dist/application/contract8-milestone-acceptance.js +21 -99
- package/dist/application/contract8-milestone-acceptance.js.map +1 -1
- package/dist/application/contract8-milestone-read.d.ts +25 -0
- package/dist/application/contract8-milestone-read.d.ts.map +1 -0
- package/dist/application/contract8-milestone-read.js +111 -0
- package/dist/application/contract8-milestone-read.js.map +1 -0
- package/dist/application/contract9-assurance-evaluate.d.ts +3 -0
- package/dist/application/contract9-assurance-evaluate.d.ts.map +1 -0
- package/dist/application/contract9-assurance-evaluate.js +75 -0
- package/dist/application/contract9-assurance-evaluate.js.map +1 -0
- package/dist/application/contract9-assurance.d.ts +1 -2
- package/dist/application/contract9-assurance.d.ts.map +1 -1
- package/dist/application/contract9-assurance.js +2 -56
- package/dist/application/contract9-assurance.js.map +1 -1
- package/dist/application/contract9-candidate.d.ts +8 -0
- package/dist/application/contract9-candidate.d.ts.map +1 -1
- package/dist/application/contract9-candidate.js +36 -11
- package/dist/application/contract9-candidate.js.map +1 -1
- package/dist/application/contract9-mixed-mutation.d.ts +3 -0
- package/dist/application/contract9-mixed-mutation.d.ts.map +1 -1
- package/dist/application/contract9-mixed-mutation.js +5 -4
- package/dist/application/contract9-mixed-mutation.js.map +1 -1
- package/dist/application/contract9-temporal.js +1 -1
- package/dist/application/contract9-temporal.js.map +1 -1
- package/dist/application/target-actuals-analysis.d.ts.map +1 -1
- package/dist/application/target-actuals-analysis.js +10 -6
- package/dist/application/target-actuals-analysis.js.map +1 -1
- package/dist/application/target-temporal-analysis.d.ts +1 -0
- package/dist/application/target-temporal-analysis.d.ts.map +1 -1
- package/dist/application/target-temporal-analysis.js +5 -1
- package/dist/application/target-temporal-analysis.js.map +1 -1
- package/dist/cli.js +761 -87
- package/dist/cli.js.map +1 -1
- package/dist/command/contract10-discovery.d.ts +17 -0
- package/dist/command/contract10-discovery.d.ts.map +1 -0
- package/dist/command/contract10-discovery.js +304 -0
- package/dist/command/contract10-discovery.js.map +1 -0
- package/dist/command/contract10-usage.d.ts +24 -0
- package/dist/command/contract10-usage.d.ts.map +1 -0
- package/dist/command/contract10-usage.js +49 -0
- package/dist/command/contract10-usage.js.map +1 -0
- package/dist/core/index.d.ts +11 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/help/assurance-guide.js +1 -1
- package/dist/help/assurance-guide.js.map +1 -1
- package/dist/help/contract10-guide.d.ts +10 -0
- package/dist/help/contract10-guide.d.ts.map +1 -0
- package/dist/help/contract10-guide.js +244 -0
- package/dist/help/contract10-guide.js.map +1 -0
- package/dist/index.d.ts +18 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/milestone-acceptance/advance.d.ts +9 -0
- package/dist/milestone-acceptance/advance.d.ts.map +1 -1
- package/dist/milestone-acceptance/advance.js +29 -2
- package/dist/milestone-acceptance/advance.js.map +1 -1
- package/dist/milestone-acceptance/mutation.d.ts +2 -0
- package/dist/milestone-acceptance/mutation.d.ts.map +1 -1
- package/dist/milestone-acceptance/mutation.js +48 -12
- package/dist/milestone-acceptance/mutation.js.map +1 -1
- package/dist/model/source-result.d.ts +12 -0
- package/dist/model/source-result.d.ts.map +1 -0
- package/dist/model/source-result.js +16 -0
- package/dist/model/source-result.js.map +1 -0
- package/dist/mutation/advance-deletion.d.ts +1 -0
- package/dist/mutation/advance-deletion.d.ts.map +1 -1
- package/dist/mutation/advance-deletion.js +4 -1
- package/dist/mutation/advance-deletion.js.map +1 -1
- package/dist/mutation/validated-source.d.ts +30 -0
- package/dist/mutation/validated-source.d.ts.map +1 -0
- package/dist/mutation/validated-source.js +58 -0
- package/dist/mutation/validated-source.js.map +1 -0
- package/dist/node/planning-token-store.d.ts +3 -0
- package/dist/node/planning-token-store.d.ts.map +1 -0
- package/dist/node/planning-token-store.js +90 -0
- package/dist/node/planning-token-store.js.map +1 -0
- package/dist/parser/document-parser.d.ts +1 -0
- package/dist/parser/document-parser.d.ts.map +1 -1
- package/dist/parser/document-parser.js +1 -1
- package/dist/parser/document-parser.js.map +1 -1
- package/dist/perttool-vscode-private-0.0.0-d1578b9.vsix +0 -0
- package/dist/planning-pool/advance.d.ts +10 -0
- package/dist/planning-pool/advance.d.ts.map +1 -0
- package/dist/planning-pool/advance.js +158 -0
- package/dist/planning-pool/advance.js.map +1 -0
- package/dist/planning-pool/format.d.ts +12 -0
- package/dist/planning-pool/format.d.ts.map +1 -0
- package/dist/planning-pool/format.js +199 -0
- package/dist/planning-pool/format.js.map +1 -0
- package/dist/planning-pool/history-guard.d.ts +19 -0
- package/dist/planning-pool/history-guard.d.ts.map +1 -0
- package/dist/planning-pool/history-guard.js +123 -0
- package/dist/planning-pool/history-guard.js.map +1 -0
- package/dist/planning-pool/history-types.d.ts +174 -0
- package/dist/planning-pool/history-types.d.ts.map +1 -0
- package/dist/planning-pool/history-types.js +2 -0
- package/dist/planning-pool/history-types.js.map +1 -0
- package/dist/planning-pool/history.d.ts +11 -0
- package/dist/planning-pool/history.d.ts.map +1 -0
- package/dist/planning-pool/history.js +977 -0
- package/dist/planning-pool/history.js.map +1 -0
- package/dist/planning-pool/human.d.ts +9 -0
- package/dist/planning-pool/human.d.ts.map +1 -0
- package/dist/planning-pool/human.js +305 -0
- package/dist/planning-pool/human.js.map +1 -0
- package/dist/planning-pool/intent-types.d.ts +76 -0
- package/dist/planning-pool/intent-types.d.ts.map +1 -0
- package/dist/planning-pool/intent-types.js +2 -0
- package/dist/planning-pool/intent-types.js.map +1 -0
- package/dist/planning-pool/intent.d.ts +7 -0
- package/dist/planning-pool/intent.d.ts.map +1 -0
- package/dist/planning-pool/intent.js +590 -0
- package/dist/planning-pool/intent.js.map +1 -0
- package/dist/planning-pool/observation-types.d.ts +143 -0
- package/dist/planning-pool/observation-types.d.ts.map +1 -0
- package/dist/planning-pool/observation-types.js +2 -0
- package/dist/planning-pool/observation-types.js.map +1 -0
- package/dist/planning-pool/observation.d.ts +8 -0
- package/dist/planning-pool/observation.d.ts.map +1 -0
- package/dist/planning-pool/observation.js +582 -0
- package/dist/planning-pool/observation.js.map +1 -0
- package/dist/planning-pool/projection.d.ts +28 -0
- package/dist/planning-pool/projection.d.ts.map +1 -0
- package/dist/planning-pool/projection.js +148 -0
- package/dist/planning-pool/projection.js.map +1 -0
- package/dist/planning-pool/public-core.d.ts +32 -0
- package/dist/planning-pool/public-core.d.ts.map +1 -0
- package/dist/planning-pool/public-core.js +122 -0
- package/dist/planning-pool/public-core.js.map +1 -0
- package/dist/planning-pool/reshape-normalize.d.ts +15 -0
- package/dist/planning-pool/reshape-normalize.d.ts.map +1 -0
- package/dist/planning-pool/reshape-normalize.js +688 -0
- package/dist/planning-pool/reshape-normalize.js.map +1 -0
- package/dist/planning-pool/reshape-token.d.ts +20 -0
- package/dist/planning-pool/reshape-token.d.ts.map +1 -0
- package/dist/planning-pool/reshape-token.js +193 -0
- package/dist/planning-pool/reshape-token.js.map +1 -0
- package/dist/planning-pool/reshape-types.d.ts +183 -0
- package/dist/planning-pool/reshape-types.d.ts.map +1 -0
- package/dist/planning-pool/reshape-types.js +2 -0
- package/dist/planning-pool/reshape-types.js.map +1 -0
- package/dist/planning-pool/reshape.d.ts +13 -0
- package/dist/planning-pool/reshape.d.ts.map +1 -0
- package/dist/planning-pool/reshape.js +1434 -0
- package/dist/planning-pool/reshape.js.map +1 -0
- package/dist/planning-pool/source-lexical.d.ts +30 -0
- package/dist/planning-pool/source-lexical.d.ts.map +1 -0
- package/dist/planning-pool/source-lexical.js +158 -0
- package/dist/planning-pool/source-lexical.js.map +1 -0
- package/dist/planning-pool/source-types.d.ts +169 -0
- package/dist/planning-pool/source-types.d.ts.map +1 -0
- package/dist/planning-pool/source-types.js +2 -0
- package/dist/planning-pool/source-types.js.map +1 -0
- package/dist/planning-pool/source-values.d.ts +16 -0
- package/dist/planning-pool/source-values.d.ts.map +1 -0
- package/dist/planning-pool/source-values.js +120 -0
- package/dist/planning-pool/source-values.js.map +1 -0
- package/dist/planning-pool/source.d.ts +18 -0
- package/dist/planning-pool/source.d.ts.map +1 -0
- package/dist/planning-pool/source.js +811 -0
- package/dist/planning-pool/source.js.map +1 -0
- package/dist/planning-pool/window-types.d.ts +133 -0
- package/dist/planning-pool/window-types.d.ts.map +1 -0
- package/dist/planning-pool/window-types.js +2 -0
- package/dist/planning-pool/window-types.js.map +1 -0
- package/dist/planning-pool/window.d.ts +6 -0
- package/dist/planning-pool/window.d.ts.map +1 -0
- package/dist/planning-pool/window.js +625 -0
- package/dist/planning-pool/window.js.map +1 -0
- package/dist/schema/registry.d.ts +1 -1
- package/dist/schema/registry.d.ts.map +1 -1
- package/dist/schema/registry.js +17 -3
- package/dist/schema/registry.js.map +1 -1
- package/dist/temporal-schedule/format.d.ts.map +1 -1
- package/dist/temporal-schedule/format.js +4 -54
- package/dist/temporal-schedule/format.js.map +1 -1
- package/dist/temporal-schedule/source-lexical.d.ts +9 -0
- package/dist/temporal-schedule/source-lexical.d.ts.map +1 -1
- package/dist/temporal-schedule/source-lexical.js +21 -10
- package/dist/temporal-schedule/source-lexical.js.map +1 -1
- package/dist/temporal-schedule/source.d.ts.map +1 -1
- package/dist/temporal-schedule/source.js +3 -13
- package/dist/temporal-schedule/source.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/schemas/{Perttool.AdvanceResult.v3.schema.json → Perttool.AdvanceResult.v4.schema.json} +42 -4
- package/schemas/Perttool.AgentGuidanceResult.v1.schema.json +1 -1
- package/schemas/Perttool.AnalysisResult.v7.schema.json +1 -1
- package/schemas/Perttool.CheckResult.v6.schema.json +1 -1
- package/schemas/Perttool.CliError.v1.schema.json +1 -1
- package/schemas/Perttool.CommandHelpResult.v1.schema.json +2 -2
- package/schemas/Perttool.ExportResult.v1.schema.json +1 -1
- package/schemas/Perttool.FormatResult.v1.schema.json +1 -1
- package/schemas/Perttool.GuideResult.v1.schema.json +1 -1
- package/schemas/Perttool.HistoricalGraphResult.v1.schema.json +1 -1
- package/schemas/Perttool.ImportResult.v1.schema.json +1 -1
- package/schemas/Perttool.InitResult.v1.schema.json +1 -1
- package/schemas/Perttool.MilestoneAcceptanceMigrationResult.v1.schema.json +1 -1
- package/schemas/Perttool.MilestoneAcceptanceResult.v1.schema.json +1 -1
- package/schemas/Perttool.MutationResult.v6.schema.json +1 -1
- package/schemas/Perttool.NextResult.v8.schema.json +7 -4
- package/schemas/Perttool.PlanAssuranceResult.v2.schema.json +1 -1
- package/schemas/Perttool.PlanningMutationResult.v1.schema.json +567 -0
- package/schemas/Perttool.PlanningObservationRequest.v1.schema.json +6 -0
- package/schemas/Perttool.PlanningPoolResult.v1.schema.json +469 -0
- package/schemas/Perttool.PlanningReshapePreflightResult.v1.schema.json +906 -0
- package/schemas/Perttool.PlanningReshapeRequest.v1.schema.json +19 -0
- package/schemas/Perttool.ProjectHistoryResult.v1.schema.json +1 -1
- package/schemas/Perttool.ProjectResult.v5.schema.json +1 -1
- package/schemas/Perttool.SchemaResult.v1.schema.json +1 -1
- package/schemas/{Perttool.UnitMigrationResult.v4.schema.json → Perttool.UnitMigrationResult.v5.schema.json} +42 -17
- package/schemas/Perttool.VelocityObservationResult.v1.schema.json +1 -1
- package/schemas/Perttool.WindowMutationRequest.v1.schema.json +6 -0
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import { parseDeclaredCalendarValue } from "../model/calendar.js";
|
|
2
|
+
import { normalizeMaxDiagnostics, } from "../model/diagnostics.js";
|
|
3
|
+
import { compare } from "../model/rational.js";
|
|
4
|
+
import { sourceValidationResult } from "../model/source-result.js";
|
|
5
|
+
import { fieldLine, scanTemporalDeclarationBlocks, sourceLineSpan, sourceSliceSpan, splitTemporalSourceLines, } from "../temporal-schedule/source-lexical.js";
|
|
6
|
+
import { parseTemporalScheduleSource, TEMPORAL_SCHEDULE_SOURCE_CAPABILITY, } from "../temporal-schedule/source.js";
|
|
7
|
+
import { compareInstants, zoneOffsetMatches, } from "../temporal-schedule/source-values.js";
|
|
8
|
+
import { declaredPlanningGrammarVersion, malformedPlanningHeaderLines, planningFields, planningPoolBaseText, scanPlanningDeclarationBlocks, } from "./source-lexical.js";
|
|
9
|
+
import { comparePlanningCalendarValues, parsePlanningDescription, parsePlanningDuration, parsePlanningInteger, parsePlanningString, parsePlanningTags, parsePlanningWhen, } from "./source-values.js";
|
|
10
|
+
export const PLANNING_POOL_SOURCE_MODEL_VERSION = 1;
|
|
11
|
+
export const PLANNING_POOL_SOURCE_CAPABILITY = Object.freeze({
|
|
12
|
+
id: "perttool.target-grammar-9-planning-pool-source",
|
|
13
|
+
version: 1,
|
|
14
|
+
grammarVersion: 9,
|
|
15
|
+
});
|
|
16
|
+
export const PLANNING_POOL_SOURCE_LIMITS = Object.freeze({
|
|
17
|
+
sourceOrCandidateUtf8Bytes: 8_388_608,
|
|
18
|
+
works: 10_000,
|
|
19
|
+
events: 20_000,
|
|
20
|
+
activities: 20_000,
|
|
21
|
+
associationsPlusProjectionLinks: 100_000,
|
|
22
|
+
workDependencies: 100_000,
|
|
23
|
+
persistedWindows: 2_048,
|
|
24
|
+
persistedWindowMemberships: 100_000,
|
|
25
|
+
});
|
|
26
|
+
const allowedFields = Object.freeze({
|
|
27
|
+
work: new Set([
|
|
28
|
+
"title", "description", "events", "activities", "milestone_links",
|
|
29
|
+
"task_links", "depends_on",
|
|
30
|
+
]),
|
|
31
|
+
event: new Set(["title", "description", "tags", "source"]),
|
|
32
|
+
activity: new Set([
|
|
33
|
+
"title", "description", "duration", "estimate", "priority", "requires",
|
|
34
|
+
"owner", "tags", "source", "calendar", "when", "deadline",
|
|
35
|
+
]),
|
|
36
|
+
window: new Set(["title", "objective", "start", "end", "works"]),
|
|
37
|
+
work_order: new Set(),
|
|
38
|
+
});
|
|
39
|
+
const associationFieldNames = new Set([
|
|
40
|
+
"events",
|
|
41
|
+
"activities",
|
|
42
|
+
"milestone_links",
|
|
43
|
+
"task_links",
|
|
44
|
+
]);
|
|
45
|
+
const dependencyFieldNames = new Set(["depends_on"]);
|
|
46
|
+
const windowMembershipFieldNames = new Set(["works"]);
|
|
47
|
+
function diagnostic(code, message, span, entityId) {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
code,
|
|
50
|
+
severity: "error",
|
|
51
|
+
message,
|
|
52
|
+
span,
|
|
53
|
+
...(entityId === undefined ? {} : { entityId }),
|
|
54
|
+
helpTopic: "syntax",
|
|
55
|
+
data: Object.freeze({}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function addDiagnostic(context, code, message, span, entityId) {
|
|
59
|
+
context.diagnostics.push(diagnostic(code, message, span, entityId));
|
|
60
|
+
}
|
|
61
|
+
function fieldOccurrences(block, name) {
|
|
62
|
+
return planningFields(block).filter((field) => field.name === name);
|
|
63
|
+
}
|
|
64
|
+
function singleField(context, block, name, required, code = "PTPOOL-101") {
|
|
65
|
+
const values = fieldOccurrences(block, name);
|
|
66
|
+
if (values.length > 1) {
|
|
67
|
+
addDiagnostic(context, code, `Duplicate ${block.kind}.${name}`, values[1].span, block.id ?? undefined);
|
|
68
|
+
}
|
|
69
|
+
if (values.length === 0 && required) {
|
|
70
|
+
addDiagnostic(context, code, `${block.kind} ${block.id ?? "order"} requires ${name}`, block.idSpan ?? block.span, block.id ?? undefined);
|
|
71
|
+
}
|
|
72
|
+
return values[0] ?? null;
|
|
73
|
+
}
|
|
74
|
+
function validateFieldSurface(context, block) {
|
|
75
|
+
const fields = planningFields(block);
|
|
76
|
+
const covered = new Set();
|
|
77
|
+
for (const field of fields) {
|
|
78
|
+
covered.add(field.line.start);
|
|
79
|
+
for (const child of field.children)
|
|
80
|
+
covered.add(child.start);
|
|
81
|
+
if (!allowedFields[block.kind].has(field.name)) {
|
|
82
|
+
addDiagnostic(context, "PTPOOL-101", `Unknown ${block.kind} field ${field.name}`, field.span, block.id ?? undefined);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const line of block.lines) {
|
|
86
|
+
if (covered.has(line.start) || line.text === "" || /^\s*#/u.test(line.text))
|
|
87
|
+
continue;
|
|
88
|
+
addDiagnostic(context, "PTPOOL-101", `Invalid ${block.kind} field or indentation`, sourceLineSpan(line), block.id ?? undefined);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function requiredString(context, block, name, code = "PTPOOL-101") {
|
|
92
|
+
const field = singleField(context, block, name, true, code);
|
|
93
|
+
if (field === null)
|
|
94
|
+
return null;
|
|
95
|
+
const value = !field.blockStyle ? parsePlanningString(field.rawValue) : null;
|
|
96
|
+
if (value === null || value.length === 0) {
|
|
97
|
+
addDiagnostic(context, code, `${block.kind}.${name} must be a non-empty string`, field.span, block.id ?? undefined);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze({ value, span: field.valueSpan });
|
|
101
|
+
}
|
|
102
|
+
function optionalString(context, block, name) {
|
|
103
|
+
const field = singleField(context, block, name, false);
|
|
104
|
+
if (field === null)
|
|
105
|
+
return null;
|
|
106
|
+
const value = !field.blockStyle ? parsePlanningString(field.rawValue) : null;
|
|
107
|
+
if (value === null) {
|
|
108
|
+
addDiagnostic(context, "PTPOOL-101", `${block.kind}.${name} must be a string`, field.span, block.id ?? undefined);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function optionalDescription(context, block) {
|
|
114
|
+
const field = singleField(context, block, "description", false);
|
|
115
|
+
if (field === null)
|
|
116
|
+
return null;
|
|
117
|
+
const value = parsePlanningDescription(field);
|
|
118
|
+
if (value === null) {
|
|
119
|
+
addDiagnostic(context, "PTPOOL-101", `${block.kind}.description must be non-empty text`, field.span, block.id ?? undefined);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
function referenceBlock(context, block, name, code) {
|
|
124
|
+
const field = singleField(context, block, name, false, code);
|
|
125
|
+
if (field === null)
|
|
126
|
+
return Object.freeze([]);
|
|
127
|
+
if (!field.blockStyle) {
|
|
128
|
+
addDiagnostic(context, code, `${block.kind}.${name} must be a reference block`, field.span, block.id ?? undefined);
|
|
129
|
+
return Object.freeze([]);
|
|
130
|
+
}
|
|
131
|
+
const result = [];
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
for (const line of field.children) {
|
|
134
|
+
if (line.text === "" || /^\s*#/u.test(line.text))
|
|
135
|
+
continue;
|
|
136
|
+
const match = /^ ([A-Za-z][A-Za-z0-9_-]*)$/u.exec(line.text);
|
|
137
|
+
if (match === null) {
|
|
138
|
+
const diagnosticCode = line.text.includes("::") ? "PTPOOL-102" : code;
|
|
139
|
+
addDiagnostic(context, diagnosticCode, `Invalid ${block.kind}.${name} reference`, sourceLineSpan(line), block.id ?? undefined);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const id = match[1];
|
|
143
|
+
const span = sourceSliceSpan(line, 4, 4 + id.length);
|
|
144
|
+
if (seen.has(id)) {
|
|
145
|
+
addDiagnostic(context, code, `Duplicate ${block.kind}.${name} reference ${id}`, span, block.id ?? undefined);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
seen.add(id);
|
|
149
|
+
result.push(Object.freeze({ id, qualifiedId: `${context.documentId}::${id}`, span }));
|
|
150
|
+
}
|
|
151
|
+
if (result.length === 0) {
|
|
152
|
+
addDiagnostic(context, code, `${block.kind}.${name} must not be empty`, field.span, block.id ?? undefined);
|
|
153
|
+
}
|
|
154
|
+
return Object.freeze(result);
|
|
155
|
+
}
|
|
156
|
+
function planningIdentity(context, block) {
|
|
157
|
+
if (block.id === null || block.idSpan === null) {
|
|
158
|
+
throw new Error("planning entity block lost its identity");
|
|
159
|
+
}
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
id: block.id,
|
|
162
|
+
qualifiedId: `${context.documentId}::${block.id}`,
|
|
163
|
+
idSpan: block.idSpan,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function parseWork(context, block) {
|
|
167
|
+
validateFieldSurface(context, block);
|
|
168
|
+
const identity = planningIdentity(context, block);
|
|
169
|
+
const title = requiredString(context, block, "title", "PTPOOL-102");
|
|
170
|
+
if (title === null)
|
|
171
|
+
return null;
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
kind: "work",
|
|
174
|
+
...identity,
|
|
175
|
+
title: title.value,
|
|
176
|
+
description: optionalDescription(context, block),
|
|
177
|
+
events: referenceBlock(context, block, "events", "PTPOOL-104"),
|
|
178
|
+
activities: referenceBlock(context, block, "activities", "PTPOOL-104"),
|
|
179
|
+
milestoneLinks: referenceBlock(context, block, "milestone_links", "PTPOOL-104"),
|
|
180
|
+
taskLinks: referenceBlock(context, block, "task_links", "PTPOOL-104"),
|
|
181
|
+
dependsOn: referenceBlock(context, block, "depends_on", "PTPOOL-106"),
|
|
182
|
+
span: block.span,
|
|
183
|
+
titleSpan: title.span,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function parseEvent(context, block) {
|
|
187
|
+
validateFieldSurface(context, block);
|
|
188
|
+
const identity = planningIdentity(context, block);
|
|
189
|
+
const title = requiredString(context, block, "title", "PTPOOL-102");
|
|
190
|
+
if (title === null)
|
|
191
|
+
return null;
|
|
192
|
+
const tagsField = singleField(context, block, "tags", false);
|
|
193
|
+
const tags = tagsField === null ? Object.freeze([]) : parsePlanningTags(tagsField.rawValue);
|
|
194
|
+
if (tags === null) {
|
|
195
|
+
addDiagnostic(context, "PTPOOL-101", "event.tags must be a valid tag list", tagsField.span, block.id);
|
|
196
|
+
}
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
kind: "event",
|
|
199
|
+
...identity,
|
|
200
|
+
title: title.value,
|
|
201
|
+
description: optionalDescription(context, block),
|
|
202
|
+
tags: tags ?? Object.freeze([]),
|
|
203
|
+
source: optionalString(context, block, "source"),
|
|
204
|
+
span: block.span,
|
|
205
|
+
titleSpan: title.span,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function durationField(context, block, field) {
|
|
209
|
+
if (field === null)
|
|
210
|
+
return null;
|
|
211
|
+
const value = parsePlanningDuration(field.rawValue, field.valueSpan);
|
|
212
|
+
if (value === null || value.unit !== context.durationUnit) {
|
|
213
|
+
addDiagnostic(context, "PTPOOL-105", "Activity duration must be positive and use the project unit", field.span, block.id);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
function estimateField(context, block, field) {
|
|
219
|
+
if (field === null)
|
|
220
|
+
return null;
|
|
221
|
+
if (!field.blockStyle) {
|
|
222
|
+
addDiagnostic(context, "PTPOOL-105", "Activity estimate must be a block", field.span, block.id);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const values = new Map();
|
|
226
|
+
for (const line of field.children) {
|
|
227
|
+
if (line.text === "" || /^\s*#/u.test(line.text))
|
|
228
|
+
continue;
|
|
229
|
+
const match = /^ (optimistic|most_likely|pessimistic) (\S+)$/u.exec(line.text);
|
|
230
|
+
if (match === null) {
|
|
231
|
+
addDiagnostic(context, "PTPOOL-105", "Invalid Activity estimate entry", sourceLineSpan(line), block.id);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const name = match[1];
|
|
235
|
+
const span = sourceSliceSpan(line, line.text.lastIndexOf(match[2]), line.text.length);
|
|
236
|
+
const value = parsePlanningDuration(match[2], span);
|
|
237
|
+
if (value === null || value.unit !== context.durationUnit || values.has(name)) {
|
|
238
|
+
addDiagnostic(context, "PTPOOL-105", `Invalid or duplicate Activity estimate ${name}`, sourceLineSpan(line), block.id);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
values.set(name, value);
|
|
242
|
+
}
|
|
243
|
+
const optimistic = values.get("optimistic");
|
|
244
|
+
const mostLikely = values.get("most_likely");
|
|
245
|
+
const pessimistic = values.get("pessimistic");
|
|
246
|
+
if (optimistic === undefined || mostLikely === undefined || pessimistic === undefined) {
|
|
247
|
+
addDiagnostic(context, "PTPOOL-105", "Activity estimate requires optimistic, most_likely, and pessimistic", field.span, block.id);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
if (compare(optimistic.value, mostLikely.value) > 0 || compare(mostLikely.value, pessimistic.value) > 0) {
|
|
251
|
+
addDiagnostic(context, "PTPOOL-105", "Activity estimate must be ordered", field.span, block.id);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return Object.freeze({ optimistic, mostLikely, pessimistic, span: field.span });
|
|
255
|
+
}
|
|
256
|
+
function requirementsField(context, block, field) {
|
|
257
|
+
if (field === null)
|
|
258
|
+
return Object.freeze([]);
|
|
259
|
+
if (!field.blockStyle) {
|
|
260
|
+
addDiagnostic(context, "PTPOOL-105", "Activity requires must be a block", field.span, block.id);
|
|
261
|
+
return Object.freeze([]);
|
|
262
|
+
}
|
|
263
|
+
const result = [];
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
for (const line of field.children) {
|
|
266
|
+
if (line.text === "" || /^\s*#/u.test(line.text))
|
|
267
|
+
continue;
|
|
268
|
+
const match = /^ ([A-Za-z][A-Za-z0-9_-]*) (\d+)$/u.exec(line.text);
|
|
269
|
+
const units = match === null ? null : parsePlanningInteger(match[2]);
|
|
270
|
+
if (match === null || units === null || units < 1) {
|
|
271
|
+
addDiagnostic(context, "PTPOOL-105", "Invalid Activity resource requirement", sourceLineSpan(line), block.id);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const id = match[1];
|
|
275
|
+
if (seen.has(id) || context.baseEntities.get(id) !== "resource") {
|
|
276
|
+
addDiagnostic(context, "PTPOOL-105", `Unknown or duplicate Activity resource ${id}`, sourceLineSpan(line), block.id);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
seen.add(id);
|
|
280
|
+
result.push(Object.freeze({
|
|
281
|
+
resourceId: id,
|
|
282
|
+
qualifiedResourceId: `${context.documentId}::${id}`,
|
|
283
|
+
units,
|
|
284
|
+
span: sourceLineSpan(line),
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
if (result.length === 0) {
|
|
288
|
+
addDiagnostic(context, "PTPOOL-105", "Activity requires must not be empty", field.span, block.id);
|
|
289
|
+
}
|
|
290
|
+
return Object.freeze(result);
|
|
291
|
+
}
|
|
292
|
+
function whenFields(context, block) {
|
|
293
|
+
const result = [];
|
|
294
|
+
const seen = new Set();
|
|
295
|
+
for (const field of fieldOccurrences(block, "when")) {
|
|
296
|
+
const value = parsePlanningWhen(field.rawValue, field.line);
|
|
297
|
+
const key = value === null ? "" : `${value.event}:${value.direction}`;
|
|
298
|
+
if (value === null || seen.has(key)) {
|
|
299
|
+
addDiagnostic(context, "PTPOOL-105", "Invalid or duplicate Activity when bound", field.span, block.id);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
seen.add(key);
|
|
303
|
+
if (context.temporalProfile.kind === "named_zone" &&
|
|
304
|
+
!zoneOffsetMatches(context.temporalProfile.zoneId, value.value)) {
|
|
305
|
+
addDiagnostic(context, "PTPOOL-105", "Activity when offset does not match the project zone or supported range", value.value.span, block.id);
|
|
306
|
+
}
|
|
307
|
+
result.push(value);
|
|
308
|
+
}
|
|
309
|
+
for (const event of ["start", "finish"]) {
|
|
310
|
+
const earliest = result.find((value) => value.event === event && value.direction === "earliest");
|
|
311
|
+
const latest = result.find((value) => value.event === event && value.direction === "latest");
|
|
312
|
+
if (earliest !== undefined &&
|
|
313
|
+
latest !== undefined &&
|
|
314
|
+
compareInstants(earliest.value, latest.value) > 0) {
|
|
315
|
+
addDiagnostic(context, "PTPOOL-105", `Activity earliest ${event} is after latest ${event}`, latest.span, block.id);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return Object.freeze(result);
|
|
319
|
+
}
|
|
320
|
+
function optionalCalendar(context, block, name, code) {
|
|
321
|
+
const field = singleField(context, block, name, false, code);
|
|
322
|
+
if (field === null)
|
|
323
|
+
return null;
|
|
324
|
+
const value = parseDeclaredCalendarValue(field.rawValue);
|
|
325
|
+
if (value === undefined) {
|
|
326
|
+
addDiagnostic(context, code, `Invalid ${block.kind}.${name}`, field.span, block.id);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return Object.freeze(value);
|
|
330
|
+
}
|
|
331
|
+
function activityEffort(context, block) {
|
|
332
|
+
const durationRaw = singleField(context, block, "duration", false, "PTPOOL-105");
|
|
333
|
+
const estimateRaw = singleField(context, block, "estimate", false, "PTPOOL-105");
|
|
334
|
+
if (durationRaw !== null && estimateRaw !== null) {
|
|
335
|
+
addDiagnostic(context, "PTPOOL-105", "Activity cannot contain both duration and estimate", estimateRaw.span, block.id);
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze({
|
|
338
|
+
duration: durationField(context, block, durationRaw),
|
|
339
|
+
estimate: estimateField(context, block, estimateRaw),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function activityPriority(context, block) {
|
|
343
|
+
const field = singleField(context, block, "priority", false, "PTPOOL-105");
|
|
344
|
+
if (field === null)
|
|
345
|
+
return null;
|
|
346
|
+
const value = parsePlanningInteger(field.rawValue);
|
|
347
|
+
if (value === null) {
|
|
348
|
+
addDiagnostic(context, "PTPOOL-105", "Invalid Activity priority", field.span, block.id);
|
|
349
|
+
}
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
function activityTags(context, block) {
|
|
353
|
+
const field = singleField(context, block, "tags", false, "PTPOOL-105");
|
|
354
|
+
if (field === null)
|
|
355
|
+
return Object.freeze([]);
|
|
356
|
+
const value = parsePlanningTags(field.rawValue);
|
|
357
|
+
if (value === null) {
|
|
358
|
+
addDiagnostic(context, "PTPOOL-105", "Invalid Activity tags", field.span, block.id);
|
|
359
|
+
return Object.freeze([]);
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
function activityCalendarId(context, block) {
|
|
364
|
+
const field = singleField(context, block, "calendar", false, "PTPOOL-105");
|
|
365
|
+
if (field === null)
|
|
366
|
+
return null;
|
|
367
|
+
if (context.baseEntities.get(field.rawValue) !== "calendar") {
|
|
368
|
+
addDiagnostic(context, "PTPOOL-105", `Unknown Activity calendar ${field.rawValue}`, field.span, block.id);
|
|
369
|
+
}
|
|
370
|
+
return field.rawValue;
|
|
371
|
+
}
|
|
372
|
+
function parseActivity(context, block) {
|
|
373
|
+
validateFieldSurface(context, block);
|
|
374
|
+
const identity = planningIdentity(context, block);
|
|
375
|
+
const title = requiredString(context, block, "title", "PTPOOL-102");
|
|
376
|
+
const effort = activityEffort(context, block);
|
|
377
|
+
const fromSpan = block.fromSpan;
|
|
378
|
+
const toSpan = block.toSpan;
|
|
379
|
+
if (title === null || block.from === null || block.to === null || fromSpan === null || toSpan === null)
|
|
380
|
+
return null;
|
|
381
|
+
return Object.freeze({
|
|
382
|
+
kind: "activity",
|
|
383
|
+
...identity,
|
|
384
|
+
from: Object.freeze({ id: block.from, qualifiedId: `${context.documentId}::${block.from}`, span: fromSpan }),
|
|
385
|
+
to: Object.freeze({ id: block.to, qualifiedId: `${context.documentId}::${block.to}`, span: toSpan }),
|
|
386
|
+
title: title.value,
|
|
387
|
+
description: optionalDescription(context, block),
|
|
388
|
+
...effort,
|
|
389
|
+
priority: activityPriority(context, block),
|
|
390
|
+
requirements: requirementsField(context, block, singleField(context, block, "requires", false, "PTPOOL-105")),
|
|
391
|
+
owner: optionalString(context, block, "owner"),
|
|
392
|
+
tags: activityTags(context, block),
|
|
393
|
+
source: optionalString(context, block, "source"),
|
|
394
|
+
calendarId: activityCalendarId(context, block),
|
|
395
|
+
when: whenFields(context, block),
|
|
396
|
+
deadline: optionalCalendar(context, block, "deadline", "PTPOOL-105"),
|
|
397
|
+
span: block.span,
|
|
398
|
+
titleSpan: title.span,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function parseWindow(context, block) {
|
|
402
|
+
validateFieldSurface(context, block);
|
|
403
|
+
const identity = planningIdentity(context, block);
|
|
404
|
+
const title = requiredString(context, block, "title", "PTPOOL-102");
|
|
405
|
+
const objective = requiredString(context, block, "objective", "PTPOOL-107");
|
|
406
|
+
const start = optionalCalendar(context, block, "start", "PTPOOL-107");
|
|
407
|
+
const end = optionalCalendar(context, block, "end", "PTPOOL-107");
|
|
408
|
+
if (start !== null && end !== null) {
|
|
409
|
+
const comparison = comparePlanningCalendarValues(start, end);
|
|
410
|
+
if (comparison === null || comparison >= 0) {
|
|
411
|
+
addDiagnostic(context, "PTPOOL-107", "Window bounds must have one kind and satisfy start < end", block.span, block.id);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const works = referenceBlock(context, block, "works", "PTPOOL-107");
|
|
415
|
+
if (fieldOccurrences(block, "works").length === 0) {
|
|
416
|
+
addDiagnostic(context, "PTPOOL-107", `Window ${block.id} requires works`, block.idSpan, block.id);
|
|
417
|
+
}
|
|
418
|
+
if (title === null || objective === null)
|
|
419
|
+
return null;
|
|
420
|
+
return Object.freeze({
|
|
421
|
+
kind: "window",
|
|
422
|
+
...identity,
|
|
423
|
+
title: title.value,
|
|
424
|
+
objective: objective.value,
|
|
425
|
+
start,
|
|
426
|
+
end,
|
|
427
|
+
works,
|
|
428
|
+
span: block.span,
|
|
429
|
+
titleSpan: title.span,
|
|
430
|
+
objectiveSpan: objective.span,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function workOrder(context, block) {
|
|
434
|
+
if (block === undefined)
|
|
435
|
+
return Object.freeze([]);
|
|
436
|
+
const result = [];
|
|
437
|
+
const seen = new Set();
|
|
438
|
+
for (const line of block.lines) {
|
|
439
|
+
if (line.text === "" || /^\s*#/u.test(line.text))
|
|
440
|
+
continue;
|
|
441
|
+
const match = /^ ([A-Za-z][A-Za-z0-9_-]*)$/u.exec(line.text);
|
|
442
|
+
if (match === null) {
|
|
443
|
+
const diagnosticCode = line.text.includes("::") ? "PTPOOL-102" : "PTPOOL-103";
|
|
444
|
+
addDiagnostic(context, diagnosticCode, "Invalid work_order entry", sourceLineSpan(line));
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const id = match[1];
|
|
448
|
+
const span = sourceSliceSpan(line, 2, 2 + id.length);
|
|
449
|
+
if (seen.has(id)) {
|
|
450
|
+
addDiagnostic(context, "PTPOOL-103", `Duplicate work_order entry ${id}`, span, id);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
seen.add(id);
|
|
454
|
+
result.push(Object.freeze({ id, qualifiedId: `${context.documentId}::${id}`, span }));
|
|
455
|
+
}
|
|
456
|
+
return Object.freeze(result);
|
|
457
|
+
}
|
|
458
|
+
function baseEntityKind(header) {
|
|
459
|
+
if (/^(?:\uFEFF)?project /u.test(header))
|
|
460
|
+
return "project";
|
|
461
|
+
if (/^calendar /u.test(header))
|
|
462
|
+
return "calendar";
|
|
463
|
+
if (/^resource /u.test(header))
|
|
464
|
+
return "resource";
|
|
465
|
+
if (/^milestone /u.test(header))
|
|
466
|
+
return "milestone";
|
|
467
|
+
if (/^task /u.test(header))
|
|
468
|
+
return "task";
|
|
469
|
+
if (/^gate /u.test(header))
|
|
470
|
+
return "gate";
|
|
471
|
+
return /^[a-z_]+ /u.test(header) ? "other" : null;
|
|
472
|
+
}
|
|
473
|
+
function baseEntityMap(text, planningBlocks) {
|
|
474
|
+
const planningStarts = new Set(planningBlocks.map(({ header }) => header.start));
|
|
475
|
+
const result = new Map();
|
|
476
|
+
for (const line of splitTemporalSourceLines(text)) {
|
|
477
|
+
if (planningStarts.has(line.start) || /^\s/u.test(line.text))
|
|
478
|
+
continue;
|
|
479
|
+
const kind = baseEntityKind(line.text);
|
|
480
|
+
const match = /^(?:\uFEFF)?[a-z_]+ ([A-Za-z][A-Za-z0-9_-]*)/u.exec(line.text);
|
|
481
|
+
if (kind !== null && match !== null && !result.has(match[1]))
|
|
482
|
+
result.set(match[1], kind);
|
|
483
|
+
}
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
function projectDurationUnit(text) {
|
|
487
|
+
const project = scanTemporalDeclarationBlocks(text).find(({ kind }) => kind === "project");
|
|
488
|
+
const value = project?.lines.map(fieldLine).find((field) => field?.name === "duration_unit")?.rawValue;
|
|
489
|
+
if (value !== "day" && value !== "hour" && value !== "point") {
|
|
490
|
+
throw new Error("validated Grammar 8 base lost project duration unit");
|
|
491
|
+
}
|
|
492
|
+
return value;
|
|
493
|
+
}
|
|
494
|
+
function validateIdentitySurface(context, entities) {
|
|
495
|
+
const seen = new Map();
|
|
496
|
+
for (const entity of entities) {
|
|
497
|
+
if (context.baseEntities.has(entity.id) || seen.has(entity.id)) {
|
|
498
|
+
addDiagnostic(context, "PTPOOL-102", `Planning identity ${entity.id} is duplicated`, entity.idSpan, entity.id);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
seen.set(entity.id, entity.idSpan);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function planningReferenceIndex(works, events, activities) {
|
|
506
|
+
return Object.freeze({
|
|
507
|
+
workIds: new Set(works.map(({ id }) => id)),
|
|
508
|
+
eventIds: new Set(events.map(({ id }) => id)),
|
|
509
|
+
activityIds: new Set(activities.map(({ id }) => id)),
|
|
510
|
+
eventConsumers: new Map(events.map(({ id }) => [id, 0])),
|
|
511
|
+
activityConsumers: new Map(activities.map(({ id }) => [id, 0])),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function validateKnownReferences(context, references, ids, code, owner) {
|
|
515
|
+
for (const reference of references) {
|
|
516
|
+
if (!ids.has(reference.id)) {
|
|
517
|
+
addDiagnostic(context, code, `${owner} references unknown ${reference.id}`, reference.span, owner);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function countConsumers(references, consumers) {
|
|
522
|
+
for (const reference of references) {
|
|
523
|
+
consumers.set(reference.id, (consumers.get(reference.id) ?? 0) + 1);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function validateWorkReferences(context, works, index) {
|
|
527
|
+
for (const work of works) {
|
|
528
|
+
validateKnownReferences(context, work.events, index.eventIds, "PTPOOL-104", work.id);
|
|
529
|
+
validateKnownReferences(context, work.activities, index.activityIds, "PTPOOL-104", work.id);
|
|
530
|
+
validateKnownReferences(context, work.dependsOn, index.workIds, "PTPOOL-106", work.id);
|
|
531
|
+
countConsumers(work.events, index.eventConsumers);
|
|
532
|
+
countConsumers(work.activities, index.activityConsumers);
|
|
533
|
+
for (const reference of work.milestoneLinks) {
|
|
534
|
+
if (context.baseEntities.get(reference.id) !== "milestone") {
|
|
535
|
+
addDiagnostic(context, "PTPOOL-104", `Work ${work.id} links unknown Milestone ${reference.id}`, reference.span, work.id);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (const reference of work.taskLinks) {
|
|
539
|
+
if (context.baseEntities.get(reference.id) !== "task") {
|
|
540
|
+
addDiagnostic(context, "PTPOOL-104", `Work ${work.id} links unknown Task ${reference.id}`, reference.span, work.id);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const reference of work.dependsOn) {
|
|
544
|
+
if (reference.id === work.id) {
|
|
545
|
+
addDiagnostic(context, "PTPOOL-106", `Work ${work.id} cannot depend on itself`, reference.span, work.id);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function validatePlanningConsumers(context, events, activities, index) {
|
|
551
|
+
for (const event of events) {
|
|
552
|
+
if ((index.eventConsumers.get(event.id) ?? 0) === 0) {
|
|
553
|
+
addDiagnostic(context, "PTPOOL-104", `Event ${event.id} has no Work association`, event.idSpan, event.id);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const activity of activities) {
|
|
557
|
+
if ((index.activityConsumers.get(activity.id) ?? 0) === 0) {
|
|
558
|
+
addDiagnostic(context, "PTPOOL-104", `Activity ${activity.id} has no Work association`, activity.idSpan, activity.id);
|
|
559
|
+
}
|
|
560
|
+
for (const endpoint of [activity.from, activity.to]) {
|
|
561
|
+
if (!index.eventIds.has(endpoint.id) && context.baseEntities.get(endpoint.id) !== "milestone") {
|
|
562
|
+
addDiagnostic(context, "PTPOOL-105", `Activity ${activity.id} endpoint ${endpoint.id} is not an Event or Milestone`, endpoint.span, activity.id);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function validateReferences(context, works, events, activities, windows) {
|
|
568
|
+
const index = planningReferenceIndex(works, events, activities);
|
|
569
|
+
validateWorkReferences(context, works, index);
|
|
570
|
+
validatePlanningConsumers(context, events, activities, index);
|
|
571
|
+
for (const window of windows) {
|
|
572
|
+
validateKnownReferences(context, window.works, index.workIds, "PTPOOL-107", window.id);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function validateOrder(context, works, orders, order) {
|
|
576
|
+
if ((works.length === 0 && orders.length > 0) || (works.length > 0 && orders.length !== 1)) {
|
|
577
|
+
const target = orders[0]?.span ?? works[0]?.span ?? sourceLineSpan(splitTemporalSourceLines(context.text)[0]);
|
|
578
|
+
addDiagnostic(context, "PTPOOL-103", "Work requires exactly one work_order and an empty pool forbids it", target);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const workIds = new Set(works.map(({ id }) => id));
|
|
582
|
+
const orderIds = new Set(order.map(({ id }) => id));
|
|
583
|
+
for (const entry of order) {
|
|
584
|
+
if (!workIds.has(entry.id))
|
|
585
|
+
addDiagnostic(context, "PTPOOL-103", `work_order references unknown Work ${entry.id}`, entry.span, entry.id);
|
|
586
|
+
}
|
|
587
|
+
for (const work of works) {
|
|
588
|
+
if (!orderIds.has(work.id))
|
|
589
|
+
addDiagnostic(context, "PTPOOL-103", `work_order is missing Work ${work.id}`, work.idSpan, work.id);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function planningFinishOrder(ids, edges) {
|
|
593
|
+
const visited = new Set();
|
|
594
|
+
const finish = [];
|
|
595
|
+
for (const root of ids) {
|
|
596
|
+
if (visited.has(root))
|
|
597
|
+
continue;
|
|
598
|
+
const stack = [{ id: root, expanded: false }];
|
|
599
|
+
while (stack.length > 0) {
|
|
600
|
+
const item = stack.pop();
|
|
601
|
+
if (item.expanded) {
|
|
602
|
+
finish.push(item.id);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (visited.has(item.id))
|
|
606
|
+
continue;
|
|
607
|
+
visited.add(item.id);
|
|
608
|
+
stack.push({ id: item.id, expanded: true });
|
|
609
|
+
for (const next of [...(edges.get(item.id) ?? [])].reverse()) {
|
|
610
|
+
if (!visited.has(next))
|
|
611
|
+
stack.push({ id: next, expanded: false });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return Object.freeze(finish);
|
|
616
|
+
}
|
|
617
|
+
function planningStrongComponents(finish, reverse) {
|
|
618
|
+
const assigned = new Set();
|
|
619
|
+
const components = [];
|
|
620
|
+
for (const root of [...finish].reverse()) {
|
|
621
|
+
if (assigned.has(root))
|
|
622
|
+
continue;
|
|
623
|
+
const component = [];
|
|
624
|
+
const stack = [root];
|
|
625
|
+
assigned.add(root);
|
|
626
|
+
while (stack.length > 0) {
|
|
627
|
+
const id = stack.pop();
|
|
628
|
+
component.push(id);
|
|
629
|
+
for (const next of reverse.get(id) ?? []) {
|
|
630
|
+
if (!assigned.has(next)) {
|
|
631
|
+
assigned.add(next);
|
|
632
|
+
stack.push(next);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
components.push(component);
|
|
637
|
+
}
|
|
638
|
+
return Object.freeze(components.map((component) => Object.freeze(component)));
|
|
639
|
+
}
|
|
640
|
+
function dependencyCycles(works) {
|
|
641
|
+
const ids = works.map(({ id }) => id);
|
|
642
|
+
const known = new Set(ids);
|
|
643
|
+
const edges = new Map(works.map((work) => [
|
|
644
|
+
work.id,
|
|
645
|
+
work.dependsOn.map(({ id }) => id).filter((id) => known.has(id) && id !== work.id),
|
|
646
|
+
]));
|
|
647
|
+
const reverse = new Map(ids.map((id) => [id, []]));
|
|
648
|
+
for (const [from, targets] of edges) {
|
|
649
|
+
for (const to of targets)
|
|
650
|
+
reverse.get(to).push(from);
|
|
651
|
+
}
|
|
652
|
+
const rank = new Map(ids.map((id, index) => [id, index]));
|
|
653
|
+
const cycles = planningStrongComponents(planningFinishOrder(ids, edges), reverse)
|
|
654
|
+
.filter((component) => component.length > 1)
|
|
655
|
+
.map((component) => [...component].sort((left, right) => rank.get(left) - rank.get(right)));
|
|
656
|
+
return Object.freeze(cycles.sort((left, right) => rank.get(left[0]) - rank.get(right[0]))
|
|
657
|
+
.map((cycle) => Object.freeze(cycle)));
|
|
658
|
+
}
|
|
659
|
+
function validateLimits(context, parsed) {
|
|
660
|
+
const associations = parsed.works.reduce((sum, work) => sum + work.events.length + work.activities.length + work.milestoneLinks.length + work.taskLinks.length, 0);
|
|
661
|
+
const dependencies = parsed.works.reduce((sum, work) => sum + work.dependsOn.length, 0);
|
|
662
|
+
const memberships = parsed.windows.reduce((sum, window) => sum + window.works.length, 0);
|
|
663
|
+
const limits = [
|
|
664
|
+
[parsed.works.length, PLANNING_POOL_SOURCE_LIMITS.works, "Work"],
|
|
665
|
+
[parsed.events.length, PLANNING_POOL_SOURCE_LIMITS.events, "Event"],
|
|
666
|
+
[parsed.activities.length, PLANNING_POOL_SOURCE_LIMITS.activities, "Activity"],
|
|
667
|
+
[associations, PLANNING_POOL_SOURCE_LIMITS.associationsPlusProjectionLinks, "association and projection link"],
|
|
668
|
+
[dependencies, PLANNING_POOL_SOURCE_LIMITS.workDependencies, "Work dependency"],
|
|
669
|
+
[parsed.windows.length, PLANNING_POOL_SOURCE_LIMITS.persistedWindows, "Window"],
|
|
670
|
+
[memberships, PLANNING_POOL_SOURCE_LIMITS.persistedWindowMemberships, "Window membership"],
|
|
671
|
+
];
|
|
672
|
+
for (const [actual, maximum, label] of limits) {
|
|
673
|
+
if (actual > maximum)
|
|
674
|
+
addDiagnostic(context, "PTPOOL-115", `${label} count ${actual} exceeds ${maximum}`, context.blocks.at(-1)?.span ?? context.blocks[0].span);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function planningStructureLimitDiagnostics(blocks) {
|
|
678
|
+
const works = blocks.filter(({ kind }) => kind === "work");
|
|
679
|
+
const events = blocks.filter(({ kind }) => kind === "event");
|
|
680
|
+
const activities = blocks.filter(({ kind }) => kind === "activity");
|
|
681
|
+
const windows = blocks.filter(({ kind }) => kind === "window");
|
|
682
|
+
const associations = works.reduce((sum, block) => sum + referenceEntryCount(block, associationFieldNames), 0);
|
|
683
|
+
const dependencies = works.reduce((sum, block) => sum + referenceEntryCount(block, dependencyFieldNames), 0);
|
|
684
|
+
const memberships = windows.reduce((sum, block) => sum + referenceEntryCount(block, windowMembershipFieldNames), 0);
|
|
685
|
+
const counts = [
|
|
686
|
+
[works.length, PLANNING_POOL_SOURCE_LIMITS.works, "Work"],
|
|
687
|
+
[events.length, PLANNING_POOL_SOURCE_LIMITS.events, "Event"],
|
|
688
|
+
[activities.length, PLANNING_POOL_SOURCE_LIMITS.activities, "Activity"],
|
|
689
|
+
[associations, PLANNING_POOL_SOURCE_LIMITS.associationsPlusProjectionLinks, "association and projection link"],
|
|
690
|
+
[dependencies, PLANNING_POOL_SOURCE_LIMITS.workDependencies, "Work dependency"],
|
|
691
|
+
[windows.length, PLANNING_POOL_SOURCE_LIMITS.persistedWindows, "Window"],
|
|
692
|
+
[memberships, PLANNING_POOL_SOURCE_LIMITS.persistedWindowMemberships, "Window membership"],
|
|
693
|
+
];
|
|
694
|
+
const target = blocks.at(-1)?.span ?? blocks[0]?.span;
|
|
695
|
+
if (target === undefined)
|
|
696
|
+
return Object.freeze([]);
|
|
697
|
+
return Object.freeze(counts.flatMap(([actual, maximum, label]) => actual > maximum
|
|
698
|
+
? [diagnostic("PTPOOL-115", `${label} count ${actual} exceeds ${maximum}`, target)]
|
|
699
|
+
: []));
|
|
700
|
+
}
|
|
701
|
+
function parsePlanning(context) {
|
|
702
|
+
const works = context.blocks.filter(({ kind }) => kind === "work")
|
|
703
|
+
.map((block) => parseWork(context, block)).filter((value) => value !== null);
|
|
704
|
+
const events = context.blocks.filter(({ kind }) => kind === "event")
|
|
705
|
+
.map((block) => parseEvent(context, block)).filter((value) => value !== null);
|
|
706
|
+
const activities = context.blocks.filter(({ kind }) => kind === "activity")
|
|
707
|
+
.map((block) => parseActivity(context, block)).filter((value) => value !== null);
|
|
708
|
+
const windows = context.blocks.filter(({ kind }) => kind === "window")
|
|
709
|
+
.map((block) => parseWindow(context, block)).filter((value) => value !== null);
|
|
710
|
+
const orders = context.blocks.filter(({ kind }) => kind === "work_order");
|
|
711
|
+
const order = workOrder(context, orders[0]);
|
|
712
|
+
validateIdentitySurface(context, [...works, ...events, ...activities, ...windows]);
|
|
713
|
+
validateReferences(context, works, events, activities, windows);
|
|
714
|
+
validateOrder(context, works, orders, order);
|
|
715
|
+
const partial = { works, events, activities, windows, workOrder: order };
|
|
716
|
+
validateLimits(context, partial);
|
|
717
|
+
return Object.freeze({ ...partial, dependencyCycles: dependencyCycles(works) });
|
|
718
|
+
}
|
|
719
|
+
function result(grammarVersion, documentId, model, diagnostics, maximum, inheritedTruncation = false) {
|
|
720
|
+
return sourceValidationResult(grammarVersion, documentId, model, diagnostics, maximum, inheritedTruncation);
|
|
721
|
+
}
|
|
722
|
+
function legacyResult(text, grammarVersion, maximum) {
|
|
723
|
+
const base = parseTemporalScheduleSource(text, TEMPORAL_SCHEDULE_SOURCE_CAPABILITY, { maxDiagnostics: maximum });
|
|
724
|
+
return Object.freeze({
|
|
725
|
+
ok: base.ok,
|
|
726
|
+
grammarVersion: Number.isSafeInteger(grammarVersion) ? grammarVersion : null,
|
|
727
|
+
documentId: base.documentId,
|
|
728
|
+
model: null,
|
|
729
|
+
diagnostics: base.diagnostics,
|
|
730
|
+
diagnosticCounts: base.diagnosticCounts,
|
|
731
|
+
diagnosticsTruncated: base.diagnosticsTruncated,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
export function parsePlanningPoolSource(text, capability, options = {}) {
|
|
735
|
+
if (capability !== PLANNING_POOL_SOURCE_CAPABILITY) {
|
|
736
|
+
throw new TypeError("the target Grammar 9 planning-pool source capability is required");
|
|
737
|
+
}
|
|
738
|
+
const maximum = normalizeMaxDiagnostics(options.maxDiagnostics);
|
|
739
|
+
const grammarVersion = declaredPlanningGrammarVersion(text);
|
|
740
|
+
if (!Number.isSafeInteger(grammarVersion) || grammarVersion < 1 || grammarVersion > 9) {
|
|
741
|
+
const legacy = legacyResult(text, grammarVersion, maximum);
|
|
742
|
+
const target = splitTemporalSourceLines(text)[0];
|
|
743
|
+
return target === undefined ? legacy : result(null, legacy.documentId, null, [
|
|
744
|
+
...legacy.diagnostics,
|
|
745
|
+
diagnostic("PTPOOL-116", "Planning Pool source requires Grammar 1 through 9", sourceLineSpan(target)),
|
|
746
|
+
], maximum, legacy.diagnosticsTruncated);
|
|
747
|
+
}
|
|
748
|
+
if (grammarVersion < 9)
|
|
749
|
+
return legacyResult(text, grammarVersion, maximum);
|
|
750
|
+
if (new TextEncoder().encode(text).byteLength >
|
|
751
|
+
PLANNING_POOL_SOURCE_LIMITS.sourceOrCandidateUtf8Bytes) {
|
|
752
|
+
const target = splitTemporalSourceLines(text)[0];
|
|
753
|
+
return result(9, null, null, target === undefined
|
|
754
|
+
? []
|
|
755
|
+
: [diagnostic("PTPOOL-115", "Planning Pool source exceeds the byte limit", sourceLineSpan(target))], maximum);
|
|
756
|
+
}
|
|
757
|
+
const blocks = scanPlanningDeclarationBlocks(text);
|
|
758
|
+
const structureLimitDiagnostics = planningStructureLimitDiagnostics(blocks);
|
|
759
|
+
if (structureLimitDiagnostics.length > 0) {
|
|
760
|
+
return result(9, null, null, structureLimitDiagnostics, maximum);
|
|
761
|
+
}
|
|
762
|
+
const baseText = planningPoolBaseText(text, blocks);
|
|
763
|
+
const base = parseTemporalScheduleSource(baseText, TEMPORAL_SCHEDULE_SOURCE_CAPABILITY, { maxDiagnostics: 1_000 });
|
|
764
|
+
const diagnostics = [...base.diagnostics];
|
|
765
|
+
for (const line of malformedPlanningHeaderLines(text, blocks)) {
|
|
766
|
+
const code = line.text.includes("::") ? "PTPOOL-102" : "PTPOOL-101";
|
|
767
|
+
diagnostics.push(diagnostic(code, "Invalid Grammar 9 planning declaration header", sourceLineSpan(line)));
|
|
768
|
+
}
|
|
769
|
+
if (!base.ok || base.model === null || base.documentId === null) {
|
|
770
|
+
return result(9, base.documentId, null, diagnostics, maximum, base.diagnosticsTruncated);
|
|
771
|
+
}
|
|
772
|
+
const context = {
|
|
773
|
+
text,
|
|
774
|
+
documentId: base.documentId,
|
|
775
|
+
blocks,
|
|
776
|
+
diagnostics,
|
|
777
|
+
baseEntities: baseEntityMap(text, blocks),
|
|
778
|
+
durationUnit: projectDurationUnit(text),
|
|
779
|
+
temporalProfile: base.model.profile,
|
|
780
|
+
};
|
|
781
|
+
const parsed = parsePlanning(context);
|
|
782
|
+
const model = Object.freeze({
|
|
783
|
+
schemaVersion: "Perttool.PlanningPoolModel.v1",
|
|
784
|
+
modelVersion: PLANNING_POOL_SOURCE_MODEL_VERSION,
|
|
785
|
+
grammarVersion: 9,
|
|
786
|
+
documentId: base.documentId,
|
|
787
|
+
qualifiedNamespace: base.documentId,
|
|
788
|
+
base: base.model,
|
|
789
|
+
...parsed,
|
|
790
|
+
});
|
|
791
|
+
return result(9, base.documentId, model, diagnostics, maximum, base.diagnosticsTruncated);
|
|
792
|
+
}
|
|
793
|
+
export function planningPoolSourceModel(source) {
|
|
794
|
+
if (!source.ok || source.model === null) {
|
|
795
|
+
throw new TypeError("a valid Grammar 9 planning-pool source result is required");
|
|
796
|
+
}
|
|
797
|
+
return source.model;
|
|
798
|
+
}
|
|
799
|
+
function referenceEntryCount(block, names) {
|
|
800
|
+
let count = 0;
|
|
801
|
+
for (const field of planningFields(block)) {
|
|
802
|
+
if (!names.has(field.name))
|
|
803
|
+
continue;
|
|
804
|
+
for (const line of field.children) {
|
|
805
|
+
if (line.text !== "" && !/^\s*#/u.test(line.text))
|
|
806
|
+
count += 1;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return count;
|
|
810
|
+
}
|
|
811
|
+
//# sourceMappingURL=source.js.map
|