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,688 @@
|
|
|
1
|
+
import { sha256DigestUtf8 } from "../model/sha256.js";
|
|
2
|
+
export const PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION = "Perttool.PlanningReshapeRequest.v1";
|
|
3
|
+
export const PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION_2 = "Perttool.PlanningReshapeRequest.v2";
|
|
4
|
+
export const PLANNING_RESHAPE_NORMALIZATION_CONTRACT = "perttool.planning-reshape-normalization@1";
|
|
5
|
+
export const PLANNING_RESHAPE_NORMALIZATION_CONTRACT_2 = "perttool.planning-reshape-normalization@2";
|
|
6
|
+
export const PLANNING_RESHAPE_REQUEST_UTF8_LIMIT = 8_388_608;
|
|
7
|
+
export const PLANNING_RESHAPE_NORMALIZED_LIMITS = Object.freeze({
|
|
8
|
+
affectedWorks: 2_048,
|
|
9
|
+
semanticRows: 50_000,
|
|
10
|
+
relationshipDispositions: 200_000,
|
|
11
|
+
});
|
|
12
|
+
function diagnostic(message, code = "PTPOOL-110") {
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
code,
|
|
15
|
+
severity: "error",
|
|
16
|
+
message,
|
|
17
|
+
data: Object.freeze({}),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function closedRecord(value, required, optional, label, diagnostics) {
|
|
24
|
+
if (!isRecord(value)) {
|
|
25
|
+
diagnostics.push(diagnostic(`${label} must be an object`));
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const allowed = new Set([...required, ...optional]);
|
|
29
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
30
|
+
const missing = required.filter((key) => !Object.hasOwn(value, key));
|
|
31
|
+
if (unknown.length > 0)
|
|
32
|
+
diagnostics.push(diagnostic(`${label} has unknown fields: ${unknown.join(", ")}`));
|
|
33
|
+
if (missing.length > 0)
|
|
34
|
+
diagnostics.push(diagnostic(`${label} is missing fields: ${missing.join(", ")}`));
|
|
35
|
+
return unknown.length === 0 && missing.length === 0 ? value : null;
|
|
36
|
+
}
|
|
37
|
+
function stringValue(value, label, diagnostics, nonEmpty = true) {
|
|
38
|
+
if (typeof value !== "string" || (nonEmpty && value.length === 0)) {
|
|
39
|
+
diagnostics.push(diagnostic(`${label} must be ${nonEmpty ? "a non-empty" : "a"} string`));
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function integerValue(value, label, diagnostics) {
|
|
45
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
46
|
+
diagnostics.push(diagnostic(`${label} must be a non-negative safe integer`));
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
const qualifiedIdPattern = /^[A-Za-z][A-Za-z0-9_-]*::[A-Za-z][A-Za-z0-9_-]*$/u;
|
|
52
|
+
const elementIdPattern = /^[A-Za-z][A-Za-z0-9_-]*$/u;
|
|
53
|
+
const digestPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
54
|
+
function qualifiedId(value, label, diagnostics) {
|
|
55
|
+
const result = stringValue(value, label, diagnostics);
|
|
56
|
+
if (result !== null && !qualifiedIdPattern.test(result)) {
|
|
57
|
+
diagnostics.push(diagnostic(`${label} must be a fully qualified planning identity`));
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function enumValue(value, values, label, diagnostics) {
|
|
63
|
+
if (typeof value !== "string" || !values.includes(value)) {
|
|
64
|
+
diagnostics.push(diagnostic(`${label} has an unknown value`));
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function nullableQualifiedId(value, label, diagnostics) {
|
|
70
|
+
return value === null ? null : qualifiedId(value, label, diagnostics) ?? undefined;
|
|
71
|
+
}
|
|
72
|
+
function parseCreatedWork(value, diagnostics) {
|
|
73
|
+
const record = closedRecord(value, ["work_id", "title", "insert_after_work_id"], [], "created Work", diagnostics);
|
|
74
|
+
if (record === null)
|
|
75
|
+
return null;
|
|
76
|
+
const workId = qualifiedId(record["work_id"], "created Work work_id", diagnostics);
|
|
77
|
+
const title = stringValue(record["title"], "created Work title", diagnostics);
|
|
78
|
+
const anchor = nullableQualifiedId(record["insert_after_work_id"], "created Work insertion anchor", diagnostics);
|
|
79
|
+
return workId === null || title === null || anchor === undefined
|
|
80
|
+
? null
|
|
81
|
+
: Object.freeze({ work_id: workId, title, insert_after_work_id: anchor });
|
|
82
|
+
}
|
|
83
|
+
function parseWorkTitleDisposition(value, diagnostics) {
|
|
84
|
+
const record = closedRecord(value, ["work_id", "title"], [], "Work title disposition", diagnostics);
|
|
85
|
+
if (record === null)
|
|
86
|
+
return null;
|
|
87
|
+
const workId = qualifiedId(record["work_id"], "Work title disposition work_id", diagnostics);
|
|
88
|
+
const title = stringValue(record["title"], "Work title disposition title", diagnostics);
|
|
89
|
+
return workId === null || title === null
|
|
90
|
+
? null
|
|
91
|
+
: Object.freeze({ work_id: workId, title });
|
|
92
|
+
}
|
|
93
|
+
function parseExistingOrigin(record, diagnostics) {
|
|
94
|
+
const checked = closedRecord(record, ["kind", "work_id", "start_utf16", "end_utf16", "source_text"], [], "existing semantic origin", diagnostics);
|
|
95
|
+
if (checked === null || checked["kind"] !== "existing")
|
|
96
|
+
return null;
|
|
97
|
+
const workId = qualifiedId(checked["work_id"], "semantic origin work_id", diagnostics);
|
|
98
|
+
const start = integerValue(checked["start_utf16"], "semantic origin start_utf16", diagnostics);
|
|
99
|
+
const end = integerValue(checked["end_utf16"], "semantic origin end_utf16", diagnostics);
|
|
100
|
+
const sourceText = stringValue(checked["source_text"], "semantic origin source_text", diagnostics, false);
|
|
101
|
+
if (start !== null && end !== null && end < start)
|
|
102
|
+
diagnostics.push(diagnostic("semantic origin end_utf16 precedes start_utf16"));
|
|
103
|
+
return workId === null || start === null || end === null || sourceText === null || end < start
|
|
104
|
+
? null
|
|
105
|
+
: Object.freeze({ kind: "existing", work_id: workId, start_utf16: start, end_utf16: end, source_text: sourceText });
|
|
106
|
+
}
|
|
107
|
+
function parseCreatedOrigin(record, diagnostics) {
|
|
108
|
+
const checked = closedRecord(record, ["kind", "source_text", "asserted_new_meaning"], [], "created semantic origin", diagnostics);
|
|
109
|
+
if (checked === null || checked["kind"] !== "created")
|
|
110
|
+
return null;
|
|
111
|
+
const sourceText = stringValue(checked["source_text"], "created semantic origin source_text", diagnostics, false);
|
|
112
|
+
if (checked["asserted_new_meaning"] !== true)
|
|
113
|
+
diagnostics.push(diagnostic("created semantic origin requires asserted_new_meaning true"));
|
|
114
|
+
return sourceText === null || checked["asserted_new_meaning"] !== true
|
|
115
|
+
? null
|
|
116
|
+
: Object.freeze({ kind: "created", source_text: sourceText, asserted_new_meaning: true });
|
|
117
|
+
}
|
|
118
|
+
function parseOrigin(value, diagnostics) {
|
|
119
|
+
if (!isRecord(value)) {
|
|
120
|
+
diagnostics.push(diagnostic("semantic origin must be an object"));
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (value["kind"] === "existing")
|
|
124
|
+
return parseExistingOrigin(value, diagnostics);
|
|
125
|
+
if (value["kind"] === "created")
|
|
126
|
+
return parseCreatedOrigin(value, diagnostics);
|
|
127
|
+
diagnostics.push(diagnostic("semantic origin has an unknown kind"));
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function parseDestination(value, diagnostics) {
|
|
131
|
+
if (!isRecord(value)) {
|
|
132
|
+
diagnostics.push(diagnostic("semantic destination must be an object"));
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (value["kind"] === "work") {
|
|
136
|
+
const record = closedRecord(value, ["kind", "work_id", "position", "text"], [], "Work semantic destination", diagnostics);
|
|
137
|
+
if (record === null)
|
|
138
|
+
return null;
|
|
139
|
+
const workId = qualifiedId(record["work_id"], "semantic destination work_id", diagnostics);
|
|
140
|
+
const position = integerValue(record["position"], "semantic destination position", diagnostics);
|
|
141
|
+
const text = stringValue(record["text"], "semantic destination text", diagnostics, false);
|
|
142
|
+
return workId === null || position === null || text === null
|
|
143
|
+
? null
|
|
144
|
+
: Object.freeze({ kind: "work", work_id: workId, position, text });
|
|
145
|
+
}
|
|
146
|
+
if (value["kind"] === "discard") {
|
|
147
|
+
const record = closedRecord(value, ["kind"], ["reason"], "discard destination", diagnostics);
|
|
148
|
+
if (record === null)
|
|
149
|
+
return null;
|
|
150
|
+
const reason = record["reason"] === undefined
|
|
151
|
+
? undefined
|
|
152
|
+
: stringValue(record["reason"], "discard reason", diagnostics, false) ?? undefined;
|
|
153
|
+
return record["reason"] !== undefined && reason === undefined
|
|
154
|
+
? null
|
|
155
|
+
: Object.freeze({ kind: "discard", ...(reason === undefined ? {} : { reason }) });
|
|
156
|
+
}
|
|
157
|
+
diagnostics.push(diagnostic("semantic destination has an unknown kind"));
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function parseSemanticElement(value, diagnostics) {
|
|
161
|
+
const record = closedRecord(value, ["element_id", "origin", "destination"], [], "semantic element", diagnostics);
|
|
162
|
+
if (record === null)
|
|
163
|
+
return null;
|
|
164
|
+
const elementId = stringValue(record["element_id"], "semantic element_id", diagnostics);
|
|
165
|
+
if (elementId !== null && !elementIdPattern.test(elementId)) {
|
|
166
|
+
diagnostics.push(diagnostic("semantic element_id is invalid"));
|
|
167
|
+
}
|
|
168
|
+
const origin = parseOrigin(record["origin"], diagnostics);
|
|
169
|
+
const destination = parseDestination(record["destination"], diagnostics);
|
|
170
|
+
return elementId === null || !elementIdPattern.test(elementId) || origin === null || destination === null
|
|
171
|
+
? null
|
|
172
|
+
: Object.freeze({ element_id: elementId, origin, destination });
|
|
173
|
+
}
|
|
174
|
+
function parseEntityDisposition(value, diagnostics) {
|
|
175
|
+
const record = closedRecord(value, ["entity_kind", "entity_id", "action"], ["reason"], "planning entity disposition", diagnostics);
|
|
176
|
+
if (record === null)
|
|
177
|
+
return null;
|
|
178
|
+
const kind = enumValue(record["entity_kind"], ["event", "activity"], "planning entity kind", diagnostics);
|
|
179
|
+
const id = qualifiedId(record["entity_id"], "planning entity id", diagnostics);
|
|
180
|
+
const action = enumValue(record["action"], ["retain", "create", "project", "defer", "discard"], "planning entity action", diagnostics);
|
|
181
|
+
const reason = record["reason"] === undefined ? undefined : stringValue(record["reason"], "planning entity reason", diagnostics, false) ?? undefined;
|
|
182
|
+
return kind === null || id === null || action === null || (record["reason"] !== undefined && reason === undefined)
|
|
183
|
+
? null
|
|
184
|
+
: Object.freeze({ entity_kind: kind, entity_id: id, action, ...(reason === undefined ? {} : { reason }) });
|
|
185
|
+
}
|
|
186
|
+
function parseAssociationDisposition(value, diagnostics) {
|
|
187
|
+
const record = closedRecord(value, ["entity_kind", "entity_id", "origin_work_id", "destination_work_id"], [], "association disposition", diagnostics);
|
|
188
|
+
if (record === null)
|
|
189
|
+
return null;
|
|
190
|
+
const kind = enumValue(record["entity_kind"], ["event", "activity"], "association entity kind", diagnostics);
|
|
191
|
+
const entityId = qualifiedId(record["entity_id"], "association entity id", diagnostics);
|
|
192
|
+
const origin = nullableQualifiedId(record["origin_work_id"], "association origin Work", diagnostics);
|
|
193
|
+
const destination = nullableQualifiedId(record["destination_work_id"], "association destination Work", diagnostics);
|
|
194
|
+
if (origin === null && destination === null)
|
|
195
|
+
diagnostics.push(diagnostic("association disposition must retain one endpoint"));
|
|
196
|
+
return kind === null || entityId === null || origin === undefined || destination === undefined || (origin === null && destination === null)
|
|
197
|
+
? null
|
|
198
|
+
: Object.freeze({ entity_kind: kind, entity_id: entityId, origin_work_id: origin, destination_work_id: destination });
|
|
199
|
+
}
|
|
200
|
+
function parseProjectionLinkDisposition(value, diagnostics) {
|
|
201
|
+
const record = closedRecord(value, ["strict_kind", "strict_id", "origin_work_id", "destination_work_id"], [], "projection-link disposition", diagnostics);
|
|
202
|
+
if (record === null)
|
|
203
|
+
return null;
|
|
204
|
+
const kind = enumValue(record["strict_kind"], ["milestone", "task"], "strict entity kind", diagnostics);
|
|
205
|
+
const strictId = qualifiedId(record["strict_id"], "strict entity id", diagnostics);
|
|
206
|
+
const origin = nullableQualifiedId(record["origin_work_id"], "projection-link origin Work", diagnostics);
|
|
207
|
+
const destination = nullableQualifiedId(record["destination_work_id"], "projection-link destination Work", diagnostics);
|
|
208
|
+
if (origin === null && destination === null)
|
|
209
|
+
diagnostics.push(diagnostic("projection-link disposition must retain one endpoint"));
|
|
210
|
+
return kind === null || strictId === null || origin === undefined || destination === undefined || (origin === null && destination === null)
|
|
211
|
+
? null
|
|
212
|
+
: Object.freeze({ strict_kind: kind, strict_id: strictId, origin_work_id: origin, destination_work_id: destination });
|
|
213
|
+
}
|
|
214
|
+
function parseStringSet(value, label, diagnostics) {
|
|
215
|
+
if (!Array.isArray(value)) {
|
|
216
|
+
diagnostics.push(diagnostic(`${label} must be an array`));
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const result = value.map((item) => qualifiedId(item, label, diagnostics));
|
|
220
|
+
return result.some((item) => item === null)
|
|
221
|
+
? null
|
|
222
|
+
: Object.freeze(result.sort(compareUnicodeScalars));
|
|
223
|
+
}
|
|
224
|
+
function optionalQualifiedId(record, field, label, diagnostics) {
|
|
225
|
+
return record[field] === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: qualifiedId(record[field], label, diagnostics) ?? null;
|
|
228
|
+
}
|
|
229
|
+
function optionalQualifiedIdSet(record, field, label, diagnostics) {
|
|
230
|
+
return record[field] === undefined
|
|
231
|
+
? undefined
|
|
232
|
+
: parseStringSet(record[field], label, diagnostics) ?? null;
|
|
233
|
+
}
|
|
234
|
+
function optionalText(record, field, label, diagnostics) {
|
|
235
|
+
return record[field] === undefined
|
|
236
|
+
? undefined
|
|
237
|
+
: stringValue(record[field], label, diagnostics, false) ?? null;
|
|
238
|
+
}
|
|
239
|
+
function validateDependencyDispositionShape(action, finalDependent, finalPrerequisite, representedBy, diagnostics) {
|
|
240
|
+
validateDependencyEndpoints(action, finalDependent, finalPrerequisite, diagnostics);
|
|
241
|
+
validateDependencyOwners(action, representedBy, diagnostics);
|
|
242
|
+
}
|
|
243
|
+
function validateDependencyEndpoints(action, finalDependent, finalPrerequisite, diagnostics) {
|
|
244
|
+
if (action === "retain" && (finalDependent !== undefined || finalPrerequisite !== undefined)) {
|
|
245
|
+
diagnostics.push(diagnostic("retained dependency cannot declare final endpoints"));
|
|
246
|
+
}
|
|
247
|
+
if (action !== null && action !== "rebind" && (finalDependent !== undefined || finalPrerequisite !== undefined)) {
|
|
248
|
+
diagnostics.push(diagnostic("only a rebound dependency may declare final endpoints"));
|
|
249
|
+
}
|
|
250
|
+
if (action === "rebind" && (typeof finalDependent !== "string" || typeof finalPrerequisite !== "string")) {
|
|
251
|
+
diagnostics.push(diagnostic("rebound dependency requires both final endpoints"));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function validateDependencyOwners(action, representedBy, diagnostics) {
|
|
255
|
+
if (action === "represented" && (!Array.isArray(representedBy) || representedBy.length === 0)) {
|
|
256
|
+
diagnostics.push(diagnostic("represented dependency requires final owners"));
|
|
257
|
+
}
|
|
258
|
+
if (action !== null && action !== "represented" && representedBy !== undefined) {
|
|
259
|
+
diagnostics.push(diagnostic("only a represented dependency may cite final owners"));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function hasNull(values) {
|
|
263
|
+
return values.some((value) => value === null);
|
|
264
|
+
}
|
|
265
|
+
function parseDependencyDisposition(value, diagnostics) {
|
|
266
|
+
const record = closedRecord(value, ["dependent_work_id", "prerequisite_work_id", "action"], ["final_dependent_work_id", "final_prerequisite_work_id", "represented_by", "reason"], "dependency disposition", diagnostics);
|
|
267
|
+
if (record === null)
|
|
268
|
+
return null;
|
|
269
|
+
const dependent = qualifiedId(record["dependent_work_id"], "dependent Work", diagnostics);
|
|
270
|
+
const prerequisite = qualifiedId(record["prerequisite_work_id"], "prerequisite Work", diagnostics);
|
|
271
|
+
const action = enumValue(record["action"], ["retain", "create", "rebind", "represented", "no_longer_required"], "dependency action", diagnostics);
|
|
272
|
+
const finalDependent = optionalQualifiedId(record, "final_dependent_work_id", "final dependent Work", diagnostics);
|
|
273
|
+
const finalPrerequisite = optionalQualifiedId(record, "final_prerequisite_work_id", "final prerequisite Work", diagnostics);
|
|
274
|
+
const representedBy = optionalQualifiedIdSet(record, "represented_by", "represented owner", diagnostics);
|
|
275
|
+
const reason = optionalText(record, "reason", "dependency reason", diagnostics);
|
|
276
|
+
validateDependencyDispositionShape(action, finalDependent, finalPrerequisite, representedBy, diagnostics);
|
|
277
|
+
if (hasNull([dependent, prerequisite, action, finalDependent, finalPrerequisite, representedBy, reason]))
|
|
278
|
+
return null;
|
|
279
|
+
return Object.freeze({
|
|
280
|
+
dependent_work_id: dependent,
|
|
281
|
+
prerequisite_work_id: prerequisite,
|
|
282
|
+
action: action,
|
|
283
|
+
...(finalDependent === undefined ? {} : { final_dependent_work_id: finalDependent }),
|
|
284
|
+
...(finalPrerequisite === undefined ? {} : { final_prerequisite_work_id: finalPrerequisite }),
|
|
285
|
+
...(representedBy === undefined ? {} : { represented_by: representedBy }),
|
|
286
|
+
...(reason === undefined ? {} : { reason: reason }),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function parseWindowMembershipDisposition(value, diagnostics) {
|
|
290
|
+
const record = closedRecord(value, ["window_id", "origin_work_id", "destination_work_id"], [], "Window membership disposition", diagnostics);
|
|
291
|
+
if (record === null)
|
|
292
|
+
return null;
|
|
293
|
+
const windowId = qualifiedId(record["window_id"], "Window id", diagnostics);
|
|
294
|
+
const origin = qualifiedId(record["origin_work_id"], "Window origin Work", diagnostics);
|
|
295
|
+
const destination = nullableQualifiedId(record["destination_work_id"], "Window destination Work", diagnostics);
|
|
296
|
+
return windowId === null || origin === null || destination === undefined
|
|
297
|
+
? null
|
|
298
|
+
: Object.freeze({ window_id: windowId, origin_work_id: origin, destination_work_id: destination });
|
|
299
|
+
}
|
|
300
|
+
function parseResidualAction(value, diagnostics) {
|
|
301
|
+
const record = closedRecord(value, ["work_id", "text"], [], "residual-description action", diagnostics);
|
|
302
|
+
if (record === null)
|
|
303
|
+
return null;
|
|
304
|
+
const workId = qualifiedId(record["work_id"], "residual-description Work", diagnostics);
|
|
305
|
+
const text = stringValue(record["text"], "residual-description text", diagnostics, false);
|
|
306
|
+
return workId === null || text === null ? null : Object.freeze({ work_id: workId, text });
|
|
307
|
+
}
|
|
308
|
+
function parseArray(value, label, diagnostics, parse) {
|
|
309
|
+
if (!Array.isArray(value)) {
|
|
310
|
+
diagnostics.push(diagnostic(`${label} must be an array`));
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const result = value.map((item) => parse(item, diagnostics));
|
|
314
|
+
return result.some((item) => item === null)
|
|
315
|
+
? null
|
|
316
|
+
: Object.freeze(result);
|
|
317
|
+
}
|
|
318
|
+
function unique(values, key, label, diagnostics) {
|
|
319
|
+
const seen = new Set();
|
|
320
|
+
for (const value of values) {
|
|
321
|
+
const identity = key(value);
|
|
322
|
+
if (seen.has(identity)) {
|
|
323
|
+
diagnostics.push(diagnostic(`Duplicate ${label} ${identity}`));
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
seen.add(identity);
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
function compareUnicodeScalars(left, right) {
|
|
331
|
+
const leftPoints = Array.from(left, (value) => value.codePointAt(0));
|
|
332
|
+
const rightPoints = Array.from(right, (value) => value.codePointAt(0));
|
|
333
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
334
|
+
for (let index = 0; index < length; index += 1) {
|
|
335
|
+
const difference = leftPoints[index] - rightPoints[index];
|
|
336
|
+
if (difference !== 0)
|
|
337
|
+
return difference;
|
|
338
|
+
}
|
|
339
|
+
return leftPoints.length - rightPoints.length;
|
|
340
|
+
}
|
|
341
|
+
function sorted(values, key) {
|
|
342
|
+
return Object.freeze([...values].sort((left, right) => compareUnicodeScalars(key(left), key(right))));
|
|
343
|
+
}
|
|
344
|
+
function canonicalValue(value) {
|
|
345
|
+
if (value === null || typeof value !== "object")
|
|
346
|
+
return JSON.stringify(value);
|
|
347
|
+
if (Array.isArray(value))
|
|
348
|
+
return `[${value.map(canonicalValue).join(",")}]`;
|
|
349
|
+
const record = value;
|
|
350
|
+
const keys = Object.keys(record).sort(compareUnicodeScalars);
|
|
351
|
+
const fields = keys.map((key) => `${JSON.stringify(key)}:${canonicalValue(record[key])}`);
|
|
352
|
+
return `{${fields.join(",")}}`;
|
|
353
|
+
}
|
|
354
|
+
function rawRequestBytes(input) {
|
|
355
|
+
try {
|
|
356
|
+
const text = typeof input === "string" ? input : JSON.stringify(input);
|
|
357
|
+
return new TextEncoder().encode(text).byteLength;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function parsedInput(input, diagnostics) {
|
|
364
|
+
if (typeof input !== "string")
|
|
365
|
+
return input;
|
|
366
|
+
try {
|
|
367
|
+
return JSON.parse(input);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
diagnostics.push(diagnostic("Planning reshape request is not valid JSON"));
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function arrayLength(record, field) {
|
|
375
|
+
const value = record[field];
|
|
376
|
+
return Array.isArray(value) ? value.length : 0;
|
|
377
|
+
}
|
|
378
|
+
function exceedsNormalizedLimits(record, diagnostics) {
|
|
379
|
+
const relationCount = [
|
|
380
|
+
"planning_entity_dispositions",
|
|
381
|
+
"association_dispositions",
|
|
382
|
+
"projection_link_dispositions",
|
|
383
|
+
"dependency_dispositions",
|
|
384
|
+
"window_membership_dispositions",
|
|
385
|
+
].reduce((total, field) => total + arrayLength(record, field), 0);
|
|
386
|
+
const violations = [
|
|
387
|
+
arrayLength(record, "affected_work_ids") > PLANNING_RESHAPE_NORMALIZED_LIMITS.affectedWorks,
|
|
388
|
+
arrayLength(record, "semantic_elements") > PLANNING_RESHAPE_NORMALIZED_LIMITS.semanticRows,
|
|
389
|
+
relationCount > PLANNING_RESHAPE_NORMALIZED_LIMITS.relationshipDispositions,
|
|
390
|
+
];
|
|
391
|
+
if (!violations.some(Boolean))
|
|
392
|
+
return false;
|
|
393
|
+
diagnostics.push(diagnostic("Planning reshape request count limit exceeded", "PTPOOL-115"));
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
function failedNormalization(diagnostics) {
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
ok: false,
|
|
399
|
+
request: null,
|
|
400
|
+
canonicalUtf8: null,
|
|
401
|
+
preflightHash: null,
|
|
402
|
+
diagnostics: Object.freeze(diagnostics),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const requestFieldsV1 = Object.freeze([
|
|
406
|
+
"request_schema_version",
|
|
407
|
+
"normalization_contract",
|
|
408
|
+
"source_digest",
|
|
409
|
+
"intent",
|
|
410
|
+
"affected_work_ids",
|
|
411
|
+
"created_works",
|
|
412
|
+
"removed_work_ids",
|
|
413
|
+
"semantic_elements",
|
|
414
|
+
"planning_entity_dispositions",
|
|
415
|
+
"association_dispositions",
|
|
416
|
+
"projection_link_dispositions",
|
|
417
|
+
"dependency_dispositions",
|
|
418
|
+
"window_membership_dispositions",
|
|
419
|
+
"final_work_order",
|
|
420
|
+
"add_residual_description",
|
|
421
|
+
"strict_fragment",
|
|
422
|
+
"window_close",
|
|
423
|
+
]);
|
|
424
|
+
const requestFieldsV2 = Object.freeze([
|
|
425
|
+
...requestFieldsV1,
|
|
426
|
+
"work_title_dispositions",
|
|
427
|
+
]);
|
|
428
|
+
function planningRequestRecord(input, diagnostics) {
|
|
429
|
+
const byteLength = rawRequestBytes(input);
|
|
430
|
+
if (byteLength === null)
|
|
431
|
+
diagnostics.push(diagnostic("Planning reshape request cannot be serialized"));
|
|
432
|
+
if (byteLength !== null && byteLength > PLANNING_RESHAPE_REQUEST_UTF8_LIMIT) {
|
|
433
|
+
diagnostics.push(diagnostic(`Planning reshape request exceeds ${PLANNING_RESHAPE_REQUEST_UTF8_LIMIT} UTF-8 bytes`, "PTPOOL-115"));
|
|
434
|
+
}
|
|
435
|
+
const parsed = parsedInput(input, diagnostics);
|
|
436
|
+
const version = isRecord(parsed) ? parsed["request_schema_version"] : null;
|
|
437
|
+
const fields = version === PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION_2
|
|
438
|
+
? requestFieldsV2
|
|
439
|
+
: requestFieldsV1;
|
|
440
|
+
const record = closedRecord(parsed, fields, [], "Planning reshape request", diagnostics);
|
|
441
|
+
if (record !== null)
|
|
442
|
+
exceedsNormalizedLimits(record, diagnostics);
|
|
443
|
+
return diagnostics.length === 0 ? record : null;
|
|
444
|
+
}
|
|
445
|
+
function nullableStringValue(value, label, diagnostics) {
|
|
446
|
+
return value === null ? null : stringValue(value, label, diagnostics) ?? undefined;
|
|
447
|
+
}
|
|
448
|
+
function parseExistingCarryTarget(value, diagnostics) {
|
|
449
|
+
const record = closedRecord(value, ["kind", "window_id"], [], "existing carry-over target", diagnostics);
|
|
450
|
+
const windowId = record === null ? null : qualifiedId(record["window_id"], "carry-over Window", diagnostics);
|
|
451
|
+
return record === null || record["kind"] !== "existing" || windowId === null
|
|
452
|
+
? null
|
|
453
|
+
: Object.freeze({ kind: "existing", window_id: windowId });
|
|
454
|
+
}
|
|
455
|
+
function parseNewCarryTarget(value, diagnostics) {
|
|
456
|
+
const record = closedRecord(value, ["kind", "window_id", "title", "objective", "start", "end"], [], "new carry-over target", diagnostics);
|
|
457
|
+
if (record === null || record["kind"] !== "new")
|
|
458
|
+
return null;
|
|
459
|
+
const windowId = qualifiedId(record["window_id"], "new carry-over Window", diagnostics);
|
|
460
|
+
const title = stringValue(record["title"], "new carry-over Window title", diagnostics);
|
|
461
|
+
const objective = stringValue(record["objective"], "new carry-over Window objective", diagnostics);
|
|
462
|
+
const start = nullableStringValue(record["start"], "new carry-over Window start", diagnostics);
|
|
463
|
+
const end = nullableStringValue(record["end"], "new carry-over Window end", diagnostics);
|
|
464
|
+
return windowId === null || title === null || objective === null || start === undefined || end === undefined
|
|
465
|
+
? null
|
|
466
|
+
: Object.freeze({ kind: "new", window_id: windowId, title, objective, start, end });
|
|
467
|
+
}
|
|
468
|
+
function parseCarryTarget(value, diagnostics) {
|
|
469
|
+
if (value === null)
|
|
470
|
+
return null;
|
|
471
|
+
const kind = isRecord(value) ? value["kind"] : undefined;
|
|
472
|
+
if (kind === "existing")
|
|
473
|
+
return parseExistingCarryTarget(value, diagnostics);
|
|
474
|
+
if (kind === "new")
|
|
475
|
+
return parseNewCarryTarget(value, diagnostics);
|
|
476
|
+
diagnostics.push(diagnostic("carry_over_target must be null, existing, or new"));
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
function parseWindowClose(value, diagnostics) {
|
|
480
|
+
if (value === null)
|
|
481
|
+
return null;
|
|
482
|
+
const record = closedRecord(value, ["window_id", "objective_disposition", "carry_over_work_ids", "carry_over_target"], [], "Window close", diagnostics);
|
|
483
|
+
if (record === null)
|
|
484
|
+
return null;
|
|
485
|
+
const windowId = qualifiedId(record["window_id"], "closed Window", diagnostics);
|
|
486
|
+
const works = parseStringSet(record["carry_over_work_ids"], "carry-over Work", diagnostics);
|
|
487
|
+
const target = parseCarryTarget(record["carry_over_target"], diagnostics);
|
|
488
|
+
if (record["objective_disposition"] !== "discard")
|
|
489
|
+
diagnostics.push(diagnostic("Window close must discard its objective"));
|
|
490
|
+
if (works !== null && (works.length === 0) !== (target === null)) {
|
|
491
|
+
diagnostics.push(diagnostic("Window close requires a target exactly when carry-over is non-empty"));
|
|
492
|
+
}
|
|
493
|
+
return windowId === null || works === null || record["objective_disposition"] !== "discard"
|
|
494
|
+
? null
|
|
495
|
+
: Object.freeze({
|
|
496
|
+
window_id: windowId,
|
|
497
|
+
objective_disposition: "discard",
|
|
498
|
+
carry_over_work_ids: works,
|
|
499
|
+
carry_over_target: target,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function parseStrictFragment(value, diagnostics) {
|
|
503
|
+
if (value === null)
|
|
504
|
+
return null;
|
|
505
|
+
const base = closedRecord(value, ["kind"], ["event_ids", "activity_ids", "task_ids", "milestone_ids"], "strict fragment", diagnostics);
|
|
506
|
+
if (base === null)
|
|
507
|
+
return null;
|
|
508
|
+
const kind = enumValue(base["kind"], ["project", "defer"], "strict fragment kind", diagnostics);
|
|
509
|
+
if (kind === "project") {
|
|
510
|
+
const exact = closedRecord(value, ["kind", "event_ids", "activity_ids"], [], "projection strict fragment", diagnostics);
|
|
511
|
+
if (exact === null)
|
|
512
|
+
return null;
|
|
513
|
+
const events = parseStringSet(exact["event_ids"], "projection Event", diagnostics);
|
|
514
|
+
const activities = parseStringSet(exact["activity_ids"], "projection Activity", diagnostics);
|
|
515
|
+
return events === null || activities === null
|
|
516
|
+
? null
|
|
517
|
+
: Object.freeze({ kind, event_ids: events, activity_ids: activities });
|
|
518
|
+
}
|
|
519
|
+
if (kind === "defer") {
|
|
520
|
+
const exact = closedRecord(value, ["kind", "task_ids", "milestone_ids"], [], "deferral strict fragment", diagnostics);
|
|
521
|
+
if (exact === null)
|
|
522
|
+
return null;
|
|
523
|
+
const tasks = parseStringSet(exact["task_ids"], "deferral Task", diagnostics);
|
|
524
|
+
const milestones = parseStringSet(exact["milestone_ids"], "deferral Milestone", diagnostics);
|
|
525
|
+
return tasks === null || milestones === null
|
|
526
|
+
? null
|
|
527
|
+
: Object.freeze({ kind, task_ids: tasks, milestone_ids: milestones });
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
function parseFinalWorkOrder(value, diagnostics) {
|
|
532
|
+
if (!Array.isArray(value)) {
|
|
533
|
+
diagnostics.push(diagnostic("final_work_order must be an array"));
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
return Object.freeze(value.map((item) => qualifiedId(item, "final_work_order", diagnostics)).filter((item) => item !== null));
|
|
537
|
+
}
|
|
538
|
+
function parseRequestFields(record, diagnostics) {
|
|
539
|
+
const version = record["request_schema_version"] === PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION_2
|
|
540
|
+
? PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION_2
|
|
541
|
+
: PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION;
|
|
542
|
+
const sourceDigest = stringValue(record["source_digest"], "source_digest", diagnostics);
|
|
543
|
+
if (sourceDigest !== null && !digestPattern.test(sourceDigest))
|
|
544
|
+
diagnostics.push(diagnostic("source_digest must be a lowercase SHA-256 identity"));
|
|
545
|
+
const intent = enumValue(record["intent"], ["reshape", "project", "defer", "archive", "composite"], "reshape intent", diagnostics);
|
|
546
|
+
const affected = parseStringSet(record["affected_work_ids"], "affected Work", diagnostics);
|
|
547
|
+
const created = parseArray(record["created_works"], "created_works", diagnostics, parseCreatedWork);
|
|
548
|
+
const titles = version === PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION_2
|
|
549
|
+
? parseArray(record["work_title_dispositions"], "work_title_dispositions", diagnostics, parseWorkTitleDisposition)
|
|
550
|
+
: Object.freeze([]);
|
|
551
|
+
const removed = parseStringSet(record["removed_work_ids"], "removed Work", diagnostics);
|
|
552
|
+
const elements = parseArray(record["semantic_elements"], "semantic_elements", diagnostics, parseSemanticElement);
|
|
553
|
+
const entities = parseArray(record["planning_entity_dispositions"], "planning_entity_dispositions", diagnostics, parseEntityDisposition);
|
|
554
|
+
const associations = parseArray(record["association_dispositions"], "association_dispositions", diagnostics, parseAssociationDisposition);
|
|
555
|
+
const links = parseArray(record["projection_link_dispositions"], "projection_link_dispositions", diagnostics, parseProjectionLinkDisposition);
|
|
556
|
+
const dependencies = parseArray(record["dependency_dispositions"], "dependency_dispositions", diagnostics, parseDependencyDisposition);
|
|
557
|
+
const memberships = parseArray(record["window_membership_dispositions"], "window_membership_dispositions", diagnostics, parseWindowMembershipDisposition);
|
|
558
|
+
const order = parseFinalWorkOrder(record["final_work_order"], diagnostics);
|
|
559
|
+
const residual = parseArray(record["add_residual_description"], "add_residual_description", diagnostics, parseResidualAction);
|
|
560
|
+
const strictFragment = parseStrictFragment(record["strict_fragment"], diagnostics);
|
|
561
|
+
const windowClose = parseWindowClose(record["window_close"], diagnostics);
|
|
562
|
+
if (hasNull([sourceDigest, intent, affected, created, titles, removed, elements, entities, associations, links, dependencies, memberships, order, residual]))
|
|
563
|
+
return null;
|
|
564
|
+
return Object.freeze({
|
|
565
|
+
version, sourceDigest: sourceDigest, intent: intent, affected: affected, created: created, titles: titles,
|
|
566
|
+
removed: removed, elements: elements, entities: entities, associations: associations,
|
|
567
|
+
links: links, dependencies: dependencies, memberships: memberships, order: order, residual: residual,
|
|
568
|
+
strictFragment, windowClose,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function validateRequestUniqueness(fields, diagnostics) {
|
|
572
|
+
unique(fields.affected, (item) => item, "affected Work", diagnostics);
|
|
573
|
+
unique(fields.created, (item) => item.work_id, "created Work", diagnostics);
|
|
574
|
+
unique(fields.titles, (item) => item.work_id, "Work title disposition", diagnostics);
|
|
575
|
+
unique(fields.removed, (item) => item, "removed Work", diagnostics);
|
|
576
|
+
unique(fields.elements, (item) => item.element_id, "semantic element", diagnostics);
|
|
577
|
+
unique(fields.entities, (item) => `${item.entity_kind}:${item.entity_id}`, "planning entity disposition", diagnostics);
|
|
578
|
+
unique(fields.associations, (item) => `${item.entity_kind}:${item.entity_id}:${item.origin_work_id ?? "-"}:${item.destination_work_id ?? "-"}`, "association disposition", diagnostics);
|
|
579
|
+
unique(fields.links, (item) => `${item.strict_kind}:${item.strict_id}:${item.origin_work_id ?? "-"}:${item.destination_work_id ?? "-"}`, "projection-link disposition", diagnostics);
|
|
580
|
+
unique(fields.dependencies, (item) => `${item.dependent_work_id}:${item.prerequisite_work_id}`, "dependency disposition", diagnostics);
|
|
581
|
+
unique(fields.memberships, (item) => `${item.window_id}:${item.origin_work_id}`, "Window membership disposition", diagnostics);
|
|
582
|
+
unique(fields.order, (item) => item, "final Work order", diagnostics);
|
|
583
|
+
unique(fields.residual, (item) => item.work_id, "residual-description action", diagnostics);
|
|
584
|
+
if (fields.strictFragment?.kind === "project") {
|
|
585
|
+
unique(fields.strictFragment.event_ids, (item) => item, "projection Event", diagnostics);
|
|
586
|
+
unique(fields.strictFragment.activity_ids, (item) => item, "projection Activity", diagnostics);
|
|
587
|
+
}
|
|
588
|
+
if (fields.strictFragment?.kind === "defer") {
|
|
589
|
+
unique(fields.strictFragment.task_ids, (item) => item, "deferral Task", diagnostics);
|
|
590
|
+
unique(fields.strictFragment.milestone_ids, (item) => item, "deferral Milestone", diagnostics);
|
|
591
|
+
}
|
|
592
|
+
if (fields.windowClose !== null) {
|
|
593
|
+
unique(fields.windowClose.carry_over_work_ids, (item) => item, "carry-over Work", diagnostics);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function normalizedRequest(fields) {
|
|
597
|
+
const version2 = fields.version === PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION_2;
|
|
598
|
+
return Object.freeze({
|
|
599
|
+
request_schema_version: fields.version,
|
|
600
|
+
normalization_contract: version2
|
|
601
|
+
? PLANNING_RESHAPE_NORMALIZATION_CONTRACT_2
|
|
602
|
+
: PLANNING_RESHAPE_NORMALIZATION_CONTRACT,
|
|
603
|
+
source_digest: fields.sourceDigest,
|
|
604
|
+
intent: fields.intent,
|
|
605
|
+
affected_work_ids: fields.affected,
|
|
606
|
+
created_works: sorted(fields.created, (item) => item.work_id),
|
|
607
|
+
...(version2
|
|
608
|
+
? { work_title_dispositions: sorted(fields.titles, (item) => item.work_id) }
|
|
609
|
+
: {}),
|
|
610
|
+
removed_work_ids: fields.removed,
|
|
611
|
+
semantic_elements: fields.elements,
|
|
612
|
+
planning_entity_dispositions: sorted(fields.entities, (item) => `${item.entity_kind}:${item.entity_id}`),
|
|
613
|
+
association_dispositions: sorted(fields.associations, (item) => `${item.entity_kind}:${item.entity_id}:${item.origin_work_id ?? "-"}:${item.destination_work_id ?? "-"}`),
|
|
614
|
+
projection_link_dispositions: sorted(fields.links, (item) => `${item.strict_kind}:${item.strict_id}:${item.origin_work_id ?? "-"}:${item.destination_work_id ?? "-"}`),
|
|
615
|
+
dependency_dispositions: sorted(fields.dependencies, (item) => `${item.dependent_work_id}:${item.prerequisite_work_id}`),
|
|
616
|
+
window_membership_dispositions: sorted(fields.memberships, (item) => `${item.window_id}:${item.origin_work_id}`),
|
|
617
|
+
final_work_order: fields.order,
|
|
618
|
+
add_residual_description: sorted(fields.residual, (item) => item.work_id),
|
|
619
|
+
strict_fragment: fields.strictFragment,
|
|
620
|
+
window_close: fields.windowClose,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
export function planningReshapeSha256(text) {
|
|
624
|
+
return sha256DigestUtf8(text);
|
|
625
|
+
}
|
|
626
|
+
export function canonicalPlanningReshapeJson(value) {
|
|
627
|
+
return canonicalValue(value);
|
|
628
|
+
}
|
|
629
|
+
function validateRequestIdentities(record, diagnostics) {
|
|
630
|
+
const version = record["request_schema_version"];
|
|
631
|
+
if (version !== PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION && version !== PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION_2) {
|
|
632
|
+
diagnostics.push(diagnostic("Planning reshape request schema identity is unsupported"));
|
|
633
|
+
}
|
|
634
|
+
const expected = version === PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION_2
|
|
635
|
+
? PLANNING_RESHAPE_NORMALIZATION_CONTRACT_2
|
|
636
|
+
: PLANNING_RESHAPE_NORMALIZATION_CONTRACT;
|
|
637
|
+
if (record["normalization_contract"] !== expected) {
|
|
638
|
+
diagnostics.push(diagnostic("Planning reshape normalization identity is unsupported"));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function validateRequestIntent(fields, diagnostics) {
|
|
642
|
+
if (fields.version === PLANNING_RESHAPE_REQUEST_SCHEMA_VERSION &&
|
|
643
|
+
fields.dependencies.some(({ action }) => action === "create")) {
|
|
644
|
+
diagnostics.push(diagnostic("created dependency requires PlanningReshapeRequest.v2"));
|
|
645
|
+
}
|
|
646
|
+
if ((fields.intent === "project") !== (fields.strictFragment?.kind === "project")) {
|
|
647
|
+
diagnostics.push(diagnostic("project intent requires exactly one projection strict fragment"));
|
|
648
|
+
}
|
|
649
|
+
if ((fields.intent === "defer") !== (fields.strictFragment?.kind === "defer")) {
|
|
650
|
+
diagnostics.push(diagnostic("defer intent requires exactly one deferral strict fragment"));
|
|
651
|
+
}
|
|
652
|
+
if (fields.intent !== "project" && fields.intent !== "defer" && fields.strictFragment !== null) {
|
|
653
|
+
diagnostics.push(diagnostic("strict_fragment requires project or defer intent"));
|
|
654
|
+
}
|
|
655
|
+
if (fields.windowClose !== null && fields.intent !== "composite") {
|
|
656
|
+
diagnostics.push(diagnostic("window_close requires composite intent"));
|
|
657
|
+
}
|
|
658
|
+
if (fields.windowClose !== null && fields.affected.length === 0) {
|
|
659
|
+
diagnostics.push(diagnostic("window_close requires an audited affected Work set"));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function successfulNormalization(fields) {
|
|
663
|
+
const request = normalizedRequest(fields);
|
|
664
|
+
const canonicalUtf8 = canonicalValue(request);
|
|
665
|
+
return Object.freeze({
|
|
666
|
+
ok: true,
|
|
667
|
+
request,
|
|
668
|
+
canonicalUtf8,
|
|
669
|
+
preflightHash: planningReshapeSha256(canonicalUtf8),
|
|
670
|
+
diagnostics: Object.freeze([]),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
export function normalizePlanningReshapeRequest(input) {
|
|
674
|
+
const diagnostics = [];
|
|
675
|
+
const record = planningRequestRecord(input, diagnostics);
|
|
676
|
+
if (record === null)
|
|
677
|
+
return failedNormalization(diagnostics);
|
|
678
|
+
validateRequestIdentities(record, diagnostics);
|
|
679
|
+
const fields = parseRequestFields(record, diagnostics);
|
|
680
|
+
if (fields === null)
|
|
681
|
+
return failedNormalization(diagnostics);
|
|
682
|
+
validateRequestIntent(fields, diagnostics);
|
|
683
|
+
validateRequestUniqueness(fields, diagnostics);
|
|
684
|
+
if (diagnostics.length > 0)
|
|
685
|
+
return failedNormalization(diagnostics);
|
|
686
|
+
return successfulNormalization(fields);
|
|
687
|
+
}
|
|
688
|
+
//# sourceMappingURL=reshape-normalize.js.map
|