@ttsc/graph 0.30.4 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -30
- package/lib/CapturedProcessOutput.d.ts +47 -0
- package/lib/CapturedProcessOutput.js +3 -0
- package/lib/CapturedProcessOutput.js.map +1 -0
- package/lib/ILauncherOption.d.ts +22 -0
- package/lib/ILauncherOption.js +3 -0
- package/lib/ILauncherOption.js.map +1 -0
- package/lib/IProjectOptions.d.ts +18 -0
- package/lib/IProjectOptions.js +3 -0
- package/lib/IProjectOptions.js.map +1 -0
- package/lib/ParsedLauncherOptions.d.ts +13 -0
- package/lib/ParsedLauncherOptions.js +3 -0
- package/lib/ParsedLauncherOptions.js.map +1 -0
- package/lib/TtscGraphApplication.d.ts +38 -1
- package/lib/TtscGraphApplication.js +26 -1
- package/lib/TtscGraphApplication.js.map +1 -1
- package/lib/TtscGraphLauncherArguments.d.ts +115 -0
- package/lib/TtscGraphLauncherArguments.js +181 -0
- package/lib/TtscGraphLauncherArguments.js.map +1 -0
- package/lib/TtscGraphViewSnapshot.d.ts +35 -0
- package/lib/TtscGraphViewSnapshot.js +48 -0
- package/lib/TtscGraphViewSnapshot.js.map +1 -0
- package/lib/index.d.ts +9 -24
- package/lib/index.js +6 -128
- package/lib/index.js.map +1 -1
- package/lib/internal/GraphProcessTrace.d.ts +1 -0
- package/lib/internal/GraphProcessTrace.js +235 -0
- package/lib/internal/GraphProcessTrace.js.map +1 -0
- package/lib/launcherArgs.d.ts +75 -16
- package/lib/launcherArgs.js +77 -8
- package/lib/launcherArgs.js.map +1 -1
- package/lib/model/IArtifactDirectory.d.ts +26 -0
- package/lib/model/IArtifactDirectory.js +3 -0
- package/lib/model/IArtifactDirectory.js.map +1 -0
- package/lib/model/IArtifactInputs.d.ts +19 -0
- package/lib/model/IArtifactInputs.js +3 -0
- package/lib/model/IArtifactInputs.js.map +1 -0
- package/lib/model/IPublishedArtifacts.d.ts +76 -0
- package/lib/model/IPublishedArtifacts.js +3 -0
- package/lib/model/IPublishedArtifacts.js.map +1 -0
- package/lib/model/IPublishedArtifactsResident.d.ts +19 -0
- package/lib/model/IPublishedArtifactsResident.js +3 -0
- package/lib/model/IPublishedArtifactsResident.js.map +1 -0
- package/lib/model/ITtscLintDaemonTarget.d.ts +20 -0
- package/lib/model/ITtscLintDaemonTarget.js +3 -0
- package/lib/model/ITtscLintDaemonTarget.js.map +1 -0
- package/lib/model/TtscGraphLinePeer.d.ts +146 -0
- package/lib/model/TtscGraphLinePeer.js +213 -0
- package/lib/model/TtscGraphLinePeer.js.map +1 -0
- package/lib/model/TtscGraphMemory.d.ts +156 -16
- package/lib/model/TtscGraphMemory.js +228 -222
- package/lib/model/TtscGraphMemory.js.map +1 -1
- package/lib/model/TtscGraphNativeArguments.d.ts +40 -0
- package/lib/model/TtscGraphNativeArguments.js +64 -0
- package/lib/model/TtscGraphNativeArguments.js.map +1 -0
- package/lib/model/TtscGraphNodeId.d.ts +55 -3
- package/lib/model/TtscGraphNodeId.js +40 -2
- package/lib/model/TtscGraphNodeId.js.map +1 -1
- package/lib/model/TtscGraphProjection.d.ts +156 -0
- package/lib/model/TtscGraphProjection.js +390 -0
- package/lib/model/TtscGraphProjection.js.map +1 -0
- package/lib/model/TtscGraphProtocol.d.ts +28 -0
- package/lib/model/TtscGraphProtocol.js +928 -0
- package/lib/model/TtscGraphProtocol.js.map +1 -0
- package/lib/model/TtscGraphReadonly.d.ts +15 -0
- package/lib/model/TtscGraphReadonly.js +3 -0
- package/lib/model/TtscGraphReadonly.js.map +1 -0
- package/lib/model/TtscGraphRequestOptions.d.ts +16 -0
- package/lib/model/TtscGraphRequestOptions.js +3 -0
- package/lib/model/TtscGraphRequestOptions.js.map +1 -0
- package/lib/model/TtscGraphSession.d.ts +82 -36
- package/lib/model/TtscGraphSession.js +164 -1238
- package/lib/model/TtscGraphSession.js.map +1 -1
- package/lib/model/TtscGraphSessionOptions.d.ts +20 -0
- package/lib/model/TtscGraphSessionOptions.js +3 -0
- package/lib/model/TtscGraphSessionOptions.js.map +1 -0
- package/lib/model/TtscGraphSessionState.d.ts +187 -0
- package/lib/model/TtscGraphSessionState.js +417 -0
- package/lib/model/TtscGraphSessionState.js.map +1 -0
- package/lib/model/TtscGraphShardStore.d.ts +64 -3
- package/lib/model/TtscGraphShardStore.js +85 -15
- package/lib/model/TtscGraphShardStore.js.map +1 -1
- package/lib/model/TtscGraphSourceReader.d.ts +31 -8
- package/lib/model/TtscGraphSourceReader.js +37 -13
- package/lib/model/TtscGraphSourceReader.js.map +1 -1
- package/lib/model/TtscLintDaemon.d.ts +35 -17
- package/lib/model/TtscLintDaemon.js +39 -117
- package/lib/model/TtscLintDaemon.js.map +1 -1
- package/lib/model/TtscLintDaemonState.d.ts +69 -0
- package/lib/model/TtscLintDaemonState.js +166 -0
- package/lib/model/TtscLintDaemonState.js.map +1 -0
- package/lib/model/copyGraphRecords.d.ts +14 -0
- package/lib/model/copyGraphRecords.js +19 -0
- package/lib/model/copyGraphRecords.js.map +1 -0
- package/lib/model/copyGraphSnapshot.d.ts +17 -0
- package/lib/model/copyGraphSnapshot.js +33 -0
- package/lib/model/copyGraphSnapshot.js.map +1 -0
- package/lib/model/loadGraph.d.ts +19 -10
- package/lib/model/loadGraph.js +21 -12
- package/lib/model/loadGraph.js.map +1 -1
- package/lib/model/publishedArtifacts.d.ts +78 -97
- package/lib/model/publishedArtifacts.js +271 -85
- package/lib/model/publishedArtifacts.js.map +1 -1
- package/lib/nativeExecutable.d.ts +19 -8
- package/lib/nativeExecutable.js +75 -15
- package/lib/nativeExecutable.js.map +1 -1
- package/lib/reduce.d.ts +19 -45
- package/lib/reduce.js +19 -3
- package/lib/reduce.js.map +1 -1
- package/lib/resolveGraphBinary.d.ts +9 -0
- package/lib/resolveGraphBinary.js +11 -3
- package/lib/resolveGraphBinary.js.map +1 -1
- package/lib/runGraph.d.ts +21 -0
- package/lib/runGraph.js +83 -0
- package/lib/runGraph.js.map +1 -0
- package/lib/server/IResolvedGraphHandle.d.ts +22 -0
- package/lib/server/IResolvedGraphHandle.js +3 -0
- package/lib/server/IResolvedGraphHandle.js.map +1 -0
- package/lib/server/createServer.d.ts +14 -10
- package/lib/server/createServer.js +42 -39
- package/lib/server/createServer.js.map +1 -1
- package/lib/server/exportSurface.d.ts +23 -1
- package/lib/server/exportSurface.js +23 -1
- package/lib/server/exportSurface.js.map +1 -1
- package/lib/server/formatGraphFailure.d.ts +20 -0
- package/lib/server/formatGraphFailure.js +35 -0
- package/lib/server/formatGraphFailure.js.map +1 -0
- package/lib/server/pathPolicy.d.ts +70 -5
- package/lib/server/pathPolicy.js +66 -4
- package/lib/server/pathPolicy.js.map +1 -1
- package/lib/server/resolveHandle.d.ts +10 -5
- package/lib/server/resolveHandle.js +39 -7
- package/lib/server/resolveHandle.js.map +1 -1
- package/lib/server/resultAudit.d.ts +17 -14
- package/lib/server/resultAudit.js +56 -27
- package/lib/server/resultAudit.js.map +1 -1
- package/lib/server/resultNext.d.ts +26 -1
- package/lib/server/resultNext.js +14 -0
- package/lib/server/resultNext.js.map +1 -1
- package/lib/server/runDetails.d.ts +72 -15
- package/lib/server/runDetails.js +104 -69
- package/lib/server/runDetails.js.map +1 -1
- package/lib/server/runEntrypoints.d.ts +9 -0
- package/lib/server/runEntrypoints.js +9 -0
- package/lib/server/runEntrypoints.js.map +1 -1
- package/lib/server/runLookup.d.ts +13 -3
- package/lib/server/runLookup.js +19 -6
- package/lib/server/runLookup.js.map +1 -1
- package/lib/server/runOverview.d.ts +13 -4
- package/lib/server/runOverview.js +13 -4
- package/lib/server/runOverview.js.map +1 -1
- package/lib/server/runTour.d.ts +9 -0
- package/lib/server/runTour.js +33 -65
- package/lib/server/runTour.js.map +1 -1
- package/lib/server/runTrace.d.ts +25 -1
- package/lib/server/runTrace.js +133 -88
- package/lib/server/runTrace.js.map +1 -1
- package/lib/server/startServer.d.ts +16 -0
- package/lib/server/startServer.js +24 -1
- package/lib/server/startServer.js.map +1 -1
- package/lib/structures/ITtscGraphApplication.d.ts +95 -41
- package/lib/structures/ITtscGraphDecorator.d.ts +18 -0
- package/lib/structures/ITtscGraphDetails.d.ts +81 -6
- package/lib/structures/ITtscGraphDocTag.d.ts +17 -8
- package/lib/structures/ITtscGraphDump.d.ts +106 -8
- package/lib/structures/ITtscGraphEdge.d.ts +14 -2
- package/lib/structures/ITtscGraphEntrypoints.d.ts +81 -6
- package/lib/structures/ITtscGraphEscape.d.ts +24 -2
- package/lib/structures/ITtscGraphEvidence.d.ts +9 -0
- package/lib/structures/ITtscGraphLookup.d.ts +42 -9
- package/lib/structures/ITtscGraphNext.d.ts +12 -1
- package/lib/structures/ITtscGraphNode.d.ts +38 -11
- package/lib/structures/ITtscGraphOverview.d.ts +81 -6
- package/lib/structures/ITtscGraphSnapshot.d.ts +97 -2
- package/lib/structures/ITtscGraphSpan.d.ts +9 -0
- package/lib/structures/ITtscGraphTour.d.ts +83 -9
- package/lib/structures/ITtscGraphTrace.d.ts +99 -14
- package/lib/structures/RawDump.d.ts +22 -0
- package/lib/structures/RawDump.js +3 -0
- package/lib/structures/RawDump.js.map +1 -0
- package/lib/structures/RawEdge.d.ts +20 -0
- package/lib/structures/RawEdge.js +3 -0
- package/lib/structures/RawEdge.js.map +1 -0
- package/lib/structures/RawNode.d.ts +26 -0
- package/lib/structures/RawNode.js +3 -0
- package/lib/structures/RawNode.js.map +1 -0
- package/lib/structures/TtscGraphArtifactNodeKind.d.ts +9 -0
- package/lib/structures/TtscGraphArtifactNodeKind.js +9 -0
- package/lib/structures/TtscGraphArtifactNodeKind.js.map +1 -1
- package/lib/structures/TtscGraphDumpEdgeKind.d.ts +15 -1
- package/lib/structures/TtscGraphDumpNodeKind.d.ts +9 -0
- package/lib/structures/TtscGraphEdgeKind.d.ts +13 -4
- package/lib/structures/TtscGraphNodeKind.d.ts +9 -0
- package/lib/structures/TtscGraphNodeModifier.d.ts +9 -0
- package/lib/structures/ViewerLink.d.ts +20 -0
- package/lib/structures/ViewerLink.js +3 -0
- package/lib/structures/ViewerLink.js.map +1 -0
- package/lib/structures/ViewerNode.d.ts +24 -0
- package/lib/structures/ViewerNode.js +3 -0
- package/lib/structures/ViewerNode.js.map +1 -0
- package/lib/structures/ViewerPayload.d.ts +42 -0
- package/lib/structures/ViewerPayload.js +3 -0
- package/lib/structures/ViewerPayload.js.map +1 -0
- package/lib/view.d.ts +9 -0
- package/lib/view.js +203 -91
- package/lib/view.js.map +1 -1
- package/lib/viewer/viewer.js +1 -1
- package/package.json +8 -6
- package/src/CapturedProcessOutput.ts +50 -0
- package/src/ILauncherOption.ts +24 -0
- package/src/IProjectOptions.ts +19 -0
- package/src/ParsedLauncherOptions.ts +13 -0
- package/src/TtscGraphApplication.ts +38 -1
- package/src/TtscGraphLauncherArguments.ts +207 -0
- package/src/TtscGraphViewSnapshot.ts +46 -0
- package/src/index.ts +9 -160
- package/src/internal/GraphProcessTrace.ts +242 -0
- package/src/launcherArgs.ts +89 -23
- package/src/model/IArtifactDirectory.ts +27 -0
- package/src/model/IArtifactInputs.ts +21 -0
- package/src/model/IPublishedArtifacts.ts +81 -0
- package/src/model/IPublishedArtifactsResident.ts +24 -0
- package/src/model/ITtscLintDaemonTarget.ts +22 -0
- package/src/model/TtscGraphLinePeer.ts +318 -0
- package/src/model/TtscGraphMemory.ts +284 -246
- package/src/model/TtscGraphNativeArguments.ts +67 -0
- package/src/model/TtscGraphNodeId.ts +57 -3
- package/src/model/TtscGraphProjection.ts +473 -0
- package/src/model/TtscGraphProtocol.ts +90 -0
- package/src/model/TtscGraphReadonly.ts +17 -0
- package/src/model/TtscGraphRequestOptions.ts +16 -0
- package/src/model/TtscGraphSession.ts +196 -475
- package/src/model/TtscGraphSessionOptions.ts +22 -0
- package/src/model/TtscGraphSessionState.ts +558 -0
- package/src/model/TtscGraphShardStore.ts +99 -23
- package/src/model/TtscGraphSourceReader.ts +37 -15
- package/src/model/TtscLintDaemon.ts +55 -136
- package/src/model/TtscLintDaemonState.ts +174 -0
- package/src/model/copyGraphRecords.ts +17 -0
- package/src/model/copyGraphSnapshot.ts +30 -0
- package/src/model/loadGraph.ts +23 -12
- package/src/model/publishedArtifacts.ts +361 -182
- package/src/nativeExecutable.ts +93 -26
- package/src/reduce.ts +24 -51
- package/src/resolveGraphBinary.ts +13 -3
- package/src/runGraph.ts +90 -0
- package/src/server/IResolvedGraphHandle.ts +24 -0
- package/src/server/createServer.ts +16 -22
- package/src/server/exportSurface.ts +23 -1
- package/src/server/formatGraphFailure.ts +32 -0
- package/src/server/pathPolicy.ts +70 -5
- package/src/server/resolveHandle.ts +45 -14
- package/src/server/resultAudit.ts +58 -27
- package/src/server/resultNext.ts +26 -1
- package/src/server/runDetails.ts +138 -73
- package/src/server/runEntrypoints.ts +13 -1
- package/src/server/runLookup.ts +23 -7
- package/src/server/runOverview.ts +17 -5
- package/src/server/runTour.ts +44 -82
- package/src/server/runTrace.ts +175 -102
- package/src/server/startServer.ts +29 -1
- package/src/structures/ITtscGraphApplication.ts +95 -41
- package/src/structures/ITtscGraphDecorator.ts +18 -0
- package/src/structures/ITtscGraphDetails.ts +81 -6
- package/src/structures/ITtscGraphDocTag.ts +17 -8
- package/src/structures/ITtscGraphDump.ts +106 -8
- package/src/structures/ITtscGraphEdge.ts +14 -2
- package/src/structures/ITtscGraphEntrypoints.ts +81 -6
- package/src/structures/ITtscGraphEscape.ts +24 -2
- package/src/structures/ITtscGraphEvidence.ts +9 -0
- package/src/structures/ITtscGraphLookup.ts +42 -9
- package/src/structures/ITtscGraphNext.ts +12 -1
- package/src/structures/ITtscGraphNode.ts +38 -11
- package/src/structures/ITtscGraphOverview.ts +88 -6
- package/src/structures/ITtscGraphSnapshot.ts +117 -2
- package/src/structures/ITtscGraphSpan.ts +9 -0
- package/src/structures/ITtscGraphTour.ts +83 -9
- package/src/structures/ITtscGraphTrace.ts +100 -14
- package/src/structures/RawDump.ts +25 -0
- package/src/structures/RawEdge.ts +22 -0
- package/src/structures/RawNode.ts +31 -0
- package/src/structures/TtscGraphArtifactNodeKind.ts +9 -0
- package/src/structures/TtscGraphDumpEdgeKind.ts +15 -1
- package/src/structures/TtscGraphDumpNodeKind.ts +9 -0
- package/src/structures/TtscGraphEdgeKind.ts +13 -4
- package/src/structures/TtscGraphNodeKind.ts +9 -0
- package/src/structures/TtscGraphNodeModifier.ts +9 -0
- package/src/structures/ViewerLink.ts +22 -0
- package/src/structures/ViewerNode.ts +28 -0
- package/src/structures/ViewerPayload.ts +52 -0
- package/src/view.ts +208 -110
- package/src/viewer/legend.ts +119 -27
|
@@ -1,106 +1,46 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type CapabilityPluginResolver,
|
|
7
|
+
type ITtscCapabilityPlugin,
|
|
8
|
+
type ITtscCapabilityPluginResolution,
|
|
9
|
+
resolveCapabilityPluginResolution,
|
|
10
|
+
} from "ttsc";
|
|
7
11
|
|
|
12
|
+
import { GraphProcessTrace } from "../internal/GraphProcessTrace";
|
|
13
|
+
import type { IArtifactDirectory } from "./IArtifactDirectory";
|
|
14
|
+
import type { IArtifactInputs } from "./IArtifactInputs";
|
|
15
|
+
import type { IPublishedArtifacts } from "./IPublishedArtifacts";
|
|
16
|
+
import type { IPublishedArtifactsResident } from "./IPublishedArtifactsResident";
|
|
8
17
|
import { TtscLintDaemon } from "./TtscLintDaemon";
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
20
|
+
* Discover configured publishers and synchronously publish their artifact set.
|
|
21
|
+
* Input identity is captured before the graph-nodes verbs to avoid accepting a
|
|
22
|
+
* concurrent document edit as already represented by an older answer.
|
|
13
23
|
*
|
|
14
|
-
*
|
|
15
|
-
* common
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* them, and re-deriving any of them in the graph producer would be a second
|
|
23
|
-
* implementation of a published contract. So the units have to arrive from the
|
|
24
|
-
* rule.
|
|
25
|
-
*
|
|
26
|
-
* They cannot arrive in-process. `ttscgraph` is the shipped per-platform
|
|
27
|
-
* binary, never a per-project native host, so it can never have a linked
|
|
28
|
-
* plugin; and `packages/lint` is its own Go module that deliberately carries no
|
|
29
|
-
* requirement on the compiler host. What is left is the channel the host
|
|
30
|
-
* already has: a plugin declares a capability and its sidecar answers a verb,
|
|
31
|
-
* exactly as `lsp-hints` does. `resolveCapabilityPlugins` is what builds and
|
|
32
|
-
* locates those sidecars, and it is the seam `ttscserver` already uses for
|
|
33
|
-
* `capabilities.lsp`, published so a consumer outside the compiler can ask
|
|
34
|
-
* too.
|
|
35
|
-
*
|
|
36
|
-
* Nothing here knows what `@ttsc/evidence` is. It asks a lint install for
|
|
37
|
-
* whatever its configured rules published, and a project that configured none
|
|
38
|
-
* gets an empty answer.
|
|
24
|
+
* @evidence contracts/common.md#principled-implementation Capability discovery and each sidecar's project-inputs/graph-nodes verbs remain the authorities for plugin selection and artifact meaning.
|
|
25
|
+
* @evidence contracts/common.md#clear-and-simple-design The one-shot path composes discovery, input capture and publication through helpers also used by resident requests.
|
|
26
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Unavailable sidecars do not produce fabricated artifacts; no evidence-rule-specific parser or private compiler mutation substitutes for the publisher.
|
|
27
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose documents synchronous ownership and the freshness snapshot's required ordering.
|
|
28
|
+
* @evidence contracts/performance.md#efficient-algorithms Discovery runs once per publication, distinct input paths are merged and fingerprinted once, and publisher JSON is concatenated in one pass.
|
|
29
|
+
* @evidence contracts/performance.md#reuse-equivalent-work This one-shot caller has no resident sidecar owner; resident consumers use publishArtifactsResident and session freshness reuse instead of repeatedly spawning this path.
|
|
30
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources SpawnSync owns each child through completion; the exchange file is overwritten per process/project, but historical project files have no explicit reclamation yet.
|
|
31
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native paths and temporary storage use Node path/os APIs; sidecars receive argument arrays and hidden Windows child windows without shell composition.
|
|
39
32
|
*/
|
|
40
|
-
export interface IPublishedArtifacts {
|
|
41
|
-
/**
|
|
42
|
-
* Path to the JSON the native producer reads, or `null` when no configured
|
|
43
|
-
* plugin publishes one.
|
|
44
|
-
*
|
|
45
|
-
* `null` is a state rather than an absence, which is why it still carries
|
|
46
|
-
* {@link inputs}. A project that adds an evidence plugin while a session is
|
|
47
|
-
* running would otherwise never be reconsidered: nothing would be watched, so
|
|
48
|
-
* nothing could report that the answer had changed from "none" to "some".
|
|
49
|
-
*/
|
|
50
|
-
file: string | null;
|
|
51
|
-
/**
|
|
52
|
-
* Everything the answer was derived from, as paths this process can state for
|
|
53
|
-
* itself.
|
|
54
|
-
*
|
|
55
|
-
* The artifacts describe documents the compiler's Program never read, so a
|
|
56
|
-
* source edit does not move them and a document edit does not move the code
|
|
57
|
-
* graph. Refreshing them is therefore a second invalidation with its own
|
|
58
|
-
* inputs, and these are those inputs.
|
|
59
|
-
*/
|
|
60
|
-
inputs: IArtifactInputs;
|
|
61
|
-
/**
|
|
62
|
-
* The state of {@link inputs} when the answer was produced.
|
|
63
|
-
*
|
|
64
|
-
* Compared against a freshly taken one to decide whether the answer is stale.
|
|
65
|
-
* When it moved, nothing else in the session can tell: the compiler's own
|
|
66
|
-
* invalidation watches the build universe, and none of this is in it.
|
|
67
|
-
*/
|
|
68
|
-
fingerprint: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Paths an answer was derived from, split by how they are watched. */
|
|
72
|
-
export interface IArtifactInputs {
|
|
73
|
-
/** Files stated one by one. */
|
|
74
|
-
files: string[];
|
|
75
|
-
/** Directories walked, which is what notices an added or deleted file. */
|
|
76
|
-
directories: IArtifactDirectory[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** A directory watched on behalf of the pattern that named it. */
|
|
80
|
-
export interface IArtifactDirectory {
|
|
81
|
-
/** Absolute path of the directory to walk. */
|
|
82
|
-
path: string;
|
|
83
|
-
/**
|
|
84
|
-
* Whether the walk descends.
|
|
85
|
-
*
|
|
86
|
-
* Taken from the pattern rather than assumed, because assuming it is
|
|
87
|
-
* expensive in exactly the case that looks harmless: a rule declaring `*.md`
|
|
88
|
-
* has the project root for its fixed prefix, and treating that as recursive
|
|
89
|
-
* would state every file in the repository before every graph request.
|
|
90
|
-
*/
|
|
91
|
-
recursive: boolean;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
33
|
export function publishArtifacts(options: {
|
|
95
34
|
cwd: string;
|
|
96
35
|
tsconfig: string;
|
|
97
36
|
}): IPublishedArtifacts {
|
|
98
|
-
const
|
|
37
|
+
const discovery = resolveCapabilityPluginResolution({
|
|
99
38
|
capability: "graphNodes",
|
|
100
39
|
cwd: options.cwd,
|
|
101
40
|
tsconfig: options.tsconfig,
|
|
102
41
|
});
|
|
103
|
-
|
|
42
|
+
const plugins = discovery.plugins;
|
|
43
|
+
if (plugins.length === 0) return unpublished(options, discovery);
|
|
104
44
|
// The inputs are stated before the set is asked for, never after. A document
|
|
105
45
|
// edited between the two calls has to read as a change next time, and only
|
|
106
46
|
// this order gives that: a fingerprint taken first describes a state at least
|
|
@@ -112,10 +52,13 @@ export function publishArtifacts(options: {
|
|
|
112
52
|
plugins.map((plugin) => runVerb(plugin, "project-inputs", options)),
|
|
113
53
|
options,
|
|
114
54
|
);
|
|
55
|
+
const fingerprint = fingerprintInputs(inputs);
|
|
115
56
|
return assemble(
|
|
116
57
|
options,
|
|
117
58
|
inputs,
|
|
59
|
+
fingerprint,
|
|
118
60
|
plugins.map((plugin) => runVerb(plugin, "graph-nodes", options)),
|
|
61
|
+
discovery,
|
|
119
62
|
);
|
|
120
63
|
}
|
|
121
64
|
|
|
@@ -132,41 +75,101 @@ export function publishArtifacts(options: {
|
|
|
132
75
|
* so the answer is the same one either way and only its cost differs. The
|
|
133
76
|
* fallback matters more here than most: a daemon that quietly answered nothing
|
|
134
77
|
* would be indistinguishable from a project that publishes nothing.
|
|
78
|
+
*
|
|
79
|
+
* @evidence contracts/common.md#principled-implementation Both paths ask the same configured sidecar verbs; input identity is captured before publication and invalidation clears the daemon's stale Program first.
|
|
80
|
+
* @evidence contracts/common.md#clear-and-simple-design The caller supplies daemon and asynchronous resolver ownership while this function owns ordered input and artifact phases and the shared result assembly.
|
|
81
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts A failed daemon falls back to the public direct verb, not an invented empty-success result or a reimplementation of its parser.
|
|
82
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose explains resident reuse, direct-command fallback and the reason to invalidate the Program before input collection.
|
|
83
|
+
* @evidence contracts/performance.md#efficient-algorithms Independent publishers run concurrently within each of two ordered phases; deduplication avoids repeatedly hashing the same declared file path.
|
|
84
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Session-owned daemons reuse process and plugin loading only while binary, manifest and project context match; changed inputs rebuild the publication.
|
|
85
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources The session owns the resolver and daemons. Failed publication joins every selected verb and releases its proof; successful publication transfers proof release to the session. Cancellation closes and joins an active daemon or supervised direct command. One exchange file per process/project still has no historical-file reclamation.
|
|
86
|
+
* @evidence contracts/portability.md#os-neutral-implementation The fallback preserves native argument-vector invocation and Node path resolution; daemon ownership uses the same cross-platform process boundary.
|
|
135
87
|
*/
|
|
136
88
|
export async function publishArtifactsResident(
|
|
137
|
-
options: { cwd: string; tsconfig: string },
|
|
138
|
-
daemon: (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// those edits would deactivate a claim whose files now exist, which is a
|
|
151
|
-
// stale answer of exactly the kind this whole mechanism removes. What the
|
|
152
|
-
// daemon still saves is the process, the plugin load, and the configuration
|
|
153
|
-
// evaluation, which is most of the cost.
|
|
154
|
-
const inputs = readInputs(
|
|
155
|
-
await Promise.all(
|
|
156
|
-
plugins.map((plugin) =>
|
|
157
|
-
askVerb(plugin, "project-inputs", options, daemon(plugin), true),
|
|
158
|
-
),
|
|
159
|
-
),
|
|
160
|
-
options,
|
|
89
|
+
options: { cwd: string; tsconfig: string; signal?: AbortSignal },
|
|
90
|
+
daemon: (
|
|
91
|
+
plugin: ITtscCapabilityPlugin,
|
|
92
|
+
) =>
|
|
93
|
+
| Pick<TtscLintDaemon, "ask" | "close">
|
|
94
|
+
| undefined
|
|
95
|
+
| Promise<Pick<TtscLintDaemon, "ask" | "close"> | undefined>,
|
|
96
|
+
resolver: Pick<CapabilityPluginResolver, "resolve" | "runCommand">,
|
|
97
|
+
): Promise<IPublishedArtifactsResident> {
|
|
98
|
+
options.signal?.throwIfAborted();
|
|
99
|
+
const discovery = await resolver.resolve(
|
|
100
|
+
{ capability: "graphNodes", cwd: options.cwd, tsconfig: options.tsconfig },
|
|
101
|
+
{ signal: options.signal },
|
|
161
102
|
);
|
|
162
|
-
|
|
163
|
-
options
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
103
|
+
try {
|
|
104
|
+
options.signal?.throwIfAborted();
|
|
105
|
+
const plugins = discovery.plugins;
|
|
106
|
+
if (plugins.length === 0) return unpublished(options, discovery);
|
|
107
|
+
// The first request of a republish drops the daemon's warm Program. The
|
|
108
|
+
// artifacts depend on which sources exist and what they declare — that is
|
|
109
|
+
// what activates a claim — and between two republishes the developer has
|
|
110
|
+
// been editing code as well as documents. Reusing a Program from before
|
|
111
|
+
// those edits would deactivate a claim whose files now exist, which is a
|
|
112
|
+
// stale answer of exactly the kind this whole mechanism removes. What the
|
|
113
|
+
// daemon still saves is the process, the plugin load, and the configuration
|
|
114
|
+
// evaluation, which is most of the cost.
|
|
115
|
+
const inputs = readInputs(
|
|
116
|
+
await joinedPublications(
|
|
117
|
+
plugins.map(async (plugin) =>
|
|
118
|
+
askVerb(
|
|
119
|
+
plugin,
|
|
120
|
+
"project-inputs",
|
|
121
|
+
options,
|
|
122
|
+
await daemon(plugin),
|
|
123
|
+
true,
|
|
124
|
+
resolver,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
options,
|
|
129
|
+
);
|
|
130
|
+
const fingerprint = fingerprintInputs(inputs);
|
|
131
|
+
const outputs = await joinedPublications(
|
|
132
|
+
plugins.map(async (plugin) =>
|
|
133
|
+
askVerb(
|
|
134
|
+
plugin,
|
|
135
|
+
"graph-nodes",
|
|
136
|
+
options,
|
|
137
|
+
await daemon(plugin),
|
|
138
|
+
false,
|
|
139
|
+
resolver,
|
|
140
|
+
),
|
|
168
141
|
),
|
|
169
|
-
)
|
|
142
|
+
);
|
|
143
|
+
options.signal?.throwIfAborted();
|
|
144
|
+
return assemble(options, inputs, fingerprint, outputs, discovery);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
try {
|
|
147
|
+
await discovery.release();
|
|
148
|
+
} catch (cleanup) {
|
|
149
|
+
throw new AggregateError(
|
|
150
|
+
[error, cleanup],
|
|
151
|
+
"@ttsc/graph: discovery release failed",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Join every publisher before releasing a failed publication's proof. */
|
|
159
|
+
async function joinedPublications(
|
|
160
|
+
outputs: Promise<string | null>[],
|
|
161
|
+
): Promise<(string | null)[]> {
|
|
162
|
+
const results = await Promise.allSettled(outputs);
|
|
163
|
+
const failures = results.filter(
|
|
164
|
+
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
165
|
+
);
|
|
166
|
+
if (failures.length > 0)
|
|
167
|
+
throw new AggregateError(
|
|
168
|
+
failures.map((result) => result.reason),
|
|
169
|
+
"@ttsc/graph: artifact publication failed",
|
|
170
|
+
);
|
|
171
|
+
return results.map(
|
|
172
|
+
(result) => (result as PromiseFulfilledResult<string | null>).value,
|
|
170
173
|
);
|
|
171
174
|
}
|
|
172
175
|
|
|
@@ -176,7 +179,7 @@ function runVerb(
|
|
|
176
179
|
verb: string,
|
|
177
180
|
options: { cwd: string; tsconfig: string },
|
|
178
181
|
): string | null {
|
|
179
|
-
const result = spawnSync(
|
|
182
|
+
const result = GraphProcessTrace.spawnSync(
|
|
180
183
|
plugin.binary,
|
|
181
184
|
[
|
|
182
185
|
verb,
|
|
@@ -214,32 +217,90 @@ function runVerb(
|
|
|
214
217
|
async function askVerb(
|
|
215
218
|
plugin: ITtscCapabilityPlugin,
|
|
216
219
|
verb: string,
|
|
217
|
-
options: { cwd: string; tsconfig: string },
|
|
218
|
-
daemon: TtscLintDaemon | undefined,
|
|
220
|
+
options: { cwd: string; tsconfig: string; signal?: AbortSignal },
|
|
221
|
+
daemon: Pick<TtscLintDaemon, "ask" | "close"> | undefined,
|
|
219
222
|
invalidate: boolean,
|
|
223
|
+
resolver: Pick<CapabilityPluginResolver, "runCommand">,
|
|
220
224
|
): Promise<string | null> {
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
options.signal?.throwIfAborted();
|
|
226
|
+
let closing: Promise<void> | undefined;
|
|
227
|
+
const abort = () => {
|
|
228
|
+
closing = daemon?.close();
|
|
229
|
+
void closing?.catch(() => undefined);
|
|
230
|
+
};
|
|
231
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
232
|
+
if (options.signal?.aborted) abort();
|
|
233
|
+
let served: string | null | undefined;
|
|
234
|
+
let failure: unknown;
|
|
235
|
+
try {
|
|
236
|
+
served = await daemon?.ask(verb, invalidate);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
failure = error;
|
|
239
|
+
throw error;
|
|
240
|
+
} finally {
|
|
241
|
+
options.signal?.removeEventListener("abort", abort);
|
|
242
|
+
try {
|
|
243
|
+
await closing;
|
|
244
|
+
} catch (cleanup) {
|
|
245
|
+
throw new AggregateError(
|
|
246
|
+
[
|
|
247
|
+
...(failure === undefined ? [] : [failure]),
|
|
248
|
+
...(options.signal?.aborted ? [options.signal.reason] : []),
|
|
249
|
+
cleanup,
|
|
250
|
+
],
|
|
251
|
+
"@ttsc/graph: cancelled daemon cleanup failed",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
options.signal?.throwIfAborted();
|
|
256
|
+
if (served !== undefined && served !== null) return served;
|
|
257
|
+
const result = await resolver.runCommand(
|
|
258
|
+
plugin.binary,
|
|
259
|
+
[
|
|
260
|
+
verb,
|
|
261
|
+
"--cwd",
|
|
262
|
+
options.cwd,
|
|
263
|
+
"--tsconfig",
|
|
264
|
+
options.tsconfig,
|
|
265
|
+
`--plugins-json=${plugin.manifest}`,
|
|
266
|
+
...projectContextArgs(plugin),
|
|
267
|
+
],
|
|
268
|
+
{ maxBuffer: 256 * 1024 * 1024, encoding: "utf8", windowsHide: true },
|
|
269
|
+
{ signal: options.signal },
|
|
270
|
+
);
|
|
271
|
+
options.signal?.throwIfAborted();
|
|
272
|
+
return result.error ||
|
|
273
|
+
result.status !== 0 ||
|
|
274
|
+
typeof result.stdout !== "string"
|
|
275
|
+
? null
|
|
276
|
+
: result.stdout;
|
|
223
277
|
}
|
|
224
278
|
|
|
225
279
|
/** The artifact answer, from each sidecar's `graph-nodes` output. */
|
|
226
|
-
function assemble
|
|
280
|
+
function assemble<
|
|
281
|
+
Discovery extends
|
|
282
|
+
| ITtscCapabilityPluginResolution
|
|
283
|
+
| CapabilityPluginResolver.Resolution,
|
|
284
|
+
>(
|
|
227
285
|
options: { cwd: string; tsconfig: string },
|
|
228
286
|
inputs: IArtifactInputs,
|
|
287
|
+
fingerprint: string,
|
|
229
288
|
outputs: readonly (string | null)[],
|
|
230
|
-
|
|
231
|
-
|
|
289
|
+
discovery: Discovery,
|
|
290
|
+
): Omit<IPublishedArtifacts, "discovery"> & { discovery: Discovery } {
|
|
232
291
|
const published: unknown[] = [];
|
|
233
292
|
for (const output of outputs) {
|
|
234
293
|
if (output === null) continue;
|
|
235
294
|
try {
|
|
236
295
|
const parsed: unknown = JSON.parse(output);
|
|
237
|
-
if (Array.isArray(parsed))
|
|
296
|
+
if (Array.isArray(parsed))
|
|
297
|
+
for (const entryToAppend of parsed) published.push(entryToAppend);
|
|
238
298
|
} catch {
|
|
239
299
|
continue;
|
|
240
300
|
}
|
|
241
301
|
}
|
|
242
|
-
if (published.length === 0)
|
|
302
|
+
if (published.length === 0)
|
|
303
|
+
return { file: null, fingerprint, inputs, discovery };
|
|
243
304
|
|
|
244
305
|
// One file per process and project, overwritten, rather than a fresh temp
|
|
245
306
|
// directory per call. A directory per call is a leak nothing here is
|
|
@@ -259,24 +320,65 @@ function assemble(
|
|
|
259
320
|
`ttsc-graph-artifacts-${String(process.pid)}-${projectKey(options)}.json`,
|
|
260
321
|
);
|
|
261
322
|
fs.writeFileSync(file, JSON.stringify(published));
|
|
262
|
-
return { file, fingerprint, inputs };
|
|
323
|
+
return { file, fingerprint, inputs, discovery };
|
|
263
324
|
}
|
|
264
325
|
|
|
265
326
|
/**
|
|
266
327
|
* Whether the inputs an answer was derived from have moved since.
|
|
267
328
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* refresh it guards — while a `stat` per document costs less than reading one
|
|
272
|
-
* of them.
|
|
329
|
+
* Published documents are compared by content identity. The owning resolver
|
|
330
|
+
* separately qualifies discovery, including successful absence. Unavailable,
|
|
331
|
+
* incomplete or changed discovery proof requires another lookup.
|
|
273
332
|
*
|
|
274
|
-
* The
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
333
|
+
* @evidence contracts/common.md#principled-implementation The owning discovery query must still qualify this exact lookup, and exchange-file existence plus declared input identity must also hold before any publication or successful absence is reused.
|
|
334
|
+
* @evidence contracts/common.md#clear-and-simple-design Discovery freshness stays with the resolver and downstream document identity stays with fingerprintInputs; this predicate combines their independent authority.
|
|
335
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Equal size and modification time cannot substitute for file-content equivalence, and a partial no-plugin watch list cannot prove discovery stability.
|
|
336
|
+
* @evidence contracts/common.md#meaningful-documentation Native paragraphs explain content-based publication freshness, successful absence and conservative relookup for incomplete discovery proof.
|
|
337
|
+
* @evidence contracts/performance.md#efficient-algorithms Freshness hashes the distinct declared inputs and traverses watch roots once per inventory entry; its cost grows with watched bytes and entries rather than only stat count.
|
|
338
|
+
* @evidence contracts/performance.md#reuse-equivalent-work The resolver's original complete discovery proof authorizes sharing populated or empty results; changed or unproved selection requires relookup, and document fingerprints separately qualify overlay reuse.
|
|
339
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources This predicate borrows an inventory and returns a boolean; the publication and session own their storage and sidecars.
|
|
340
|
+
* @evidence contracts/portability.md#os-neutral-implementation Existence and fingerprints use Node filesystem APIs and native absolute paths without assumptions about platform shell commands.
|
|
278
341
|
*/
|
|
279
342
|
export function artifactsAreStale(published: IPublishedArtifacts): boolean {
|
|
343
|
+
// Discovery remains the loader's authority, including successful absence.
|
|
344
|
+
// A downstream input list cannot replace an unavailable or incomplete proof.
|
|
345
|
+
if (
|
|
346
|
+
published.discovery?.status !== "resolved" ||
|
|
347
|
+
!published.discovery.isCurrent()
|
|
348
|
+
)
|
|
349
|
+
return true;
|
|
350
|
+
return artifactInputsAreStale(published);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check the exact resident discovery proof before reusing its overlay.
|
|
355
|
+
* Discovery runs off the request thread; declared artifact bytes remain an
|
|
356
|
+
* independent freshness premise after that authority has answered.
|
|
357
|
+
*
|
|
358
|
+
* @evidence contracts/common.md#principled-implementation The original asynchronous proof must authorize reuse before exchange existence and declared input fingerprints are compared.
|
|
359
|
+
* @evidence contracts/common.md#clear-and-simple-design Resolver proof and artifact-input validation retain their separate owners in one ordered predicate.
|
|
360
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Unavailable or invalid proof requires relookup; no partial inventory substitutes for SDK authority.
|
|
361
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose distinguishes off-thread discovery from this consumer's independent artifact inputs.
|
|
362
|
+
* @evidence contracts/performance.md#efficient-algorithms The owner validates discovery once and fingerprintInputs hashes each declared file and directory inventory, scaling with declared entries and bytes.
|
|
363
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Only this exact publication's current proof plus unchanged artifact inputs authorizes reuse, including resolved absence.
|
|
364
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources This predicate borrows the session-owned publication and acquires no lasting proof or process.
|
|
365
|
+
* @evidence contracts/portability.md#os-neutral-implementation SDK process observation remains with its owner and artifact checks use Node native filesystem APIs.
|
|
366
|
+
*/
|
|
367
|
+
export async function artifactsAreStaleResident(
|
|
368
|
+
published: IPublishedArtifactsResident,
|
|
369
|
+
options: { signal?: AbortSignal } = {},
|
|
370
|
+
): Promise<boolean> {
|
|
371
|
+
options.signal?.throwIfAborted();
|
|
372
|
+
if (published.discovery.status !== "resolved") return true;
|
|
373
|
+
const current = await published.discovery.isCurrent(options);
|
|
374
|
+
options.signal?.throwIfAborted();
|
|
375
|
+
if (!current) return true;
|
|
376
|
+
return artifactInputsAreStale(published);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function artifactInputsAreStale(
|
|
380
|
+
published: Omit<IPublishedArtifacts, "discovery">,
|
|
381
|
+
): boolean {
|
|
280
382
|
// The written set is one of its own inputs, by existence alone. It lives in
|
|
281
383
|
// the system temp directory, which is swept on a schedule this session has no
|
|
282
384
|
// say in, and the server is handed the path on every request — so once it is
|
|
@@ -295,21 +397,19 @@ export function artifactsAreStale(published: IPublishedArtifacts): boolean {
|
|
|
295
397
|
* The answer for a project that publishes nothing, and what to watch so that
|
|
296
398
|
* answer can change.
|
|
297
399
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* samchon/ttsc#1276 is about — and paying that per request to learn nothing
|
|
303
|
-
* would be worse than the staleness it prevents.
|
|
304
|
-
*
|
|
305
|
-
* Bounded, and deliberately: a tsconfig that inherits its plugins from an
|
|
306
|
-
* extended config is not tracked here, because naming the whole extends chain
|
|
307
|
-
* means asking the loader that is itself the expense.
|
|
400
|
+
* The direct config and package manifest are useful provenance, but do not
|
|
401
|
+
* cover inherited configuration or dependency installation. The attached owning
|
|
402
|
+
* discovery proof supplies that independent authority; direct paths do not
|
|
403
|
+
* stand in for it.
|
|
308
404
|
*/
|
|
309
|
-
function unpublished
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
405
|
+
function unpublished<
|
|
406
|
+
Discovery extends
|
|
407
|
+
| ITtscCapabilityPluginResolution
|
|
408
|
+
| CapabilityPluginResolver.Resolution,
|
|
409
|
+
>(
|
|
410
|
+
options: { cwd: string; tsconfig: string },
|
|
411
|
+
discovery: Discovery,
|
|
412
|
+
): Omit<IPublishedArtifacts, "discovery"> & { discovery: Discovery } {
|
|
313
413
|
const inputs: IArtifactInputs = {
|
|
314
414
|
directories: [],
|
|
315
415
|
files: [
|
|
@@ -317,7 +417,12 @@ function unpublished(options: {
|
|
|
317
417
|
path.resolve(options.cwd, "package.json"),
|
|
318
418
|
],
|
|
319
419
|
};
|
|
320
|
-
return {
|
|
420
|
+
return {
|
|
421
|
+
file: null,
|
|
422
|
+
fingerprint: fingerprintInputs(inputs),
|
|
423
|
+
inputs,
|
|
424
|
+
discovery,
|
|
425
|
+
};
|
|
321
426
|
}
|
|
322
427
|
|
|
323
428
|
/**
|
|
@@ -399,16 +504,28 @@ function readInputs(
|
|
|
399
504
|
* Two readings have to be right here, and both are cheap to get wrong. A
|
|
400
505
|
* pattern carrying no wildcard is a path, not a tree: watching its parent
|
|
401
506
|
* instead would state every sibling on every request to learn about the one
|
|
402
|
-
* file that was declared.
|
|
403
|
-
*
|
|
404
|
-
*
|
|
507
|
+
* file that was declared. A bare filename wildcard stays shallow; globstar or a
|
|
508
|
+
* wildcard directory component requires descent from the fixed prefix.
|
|
509
|
+
*
|
|
510
|
+
* @evidence contracts/common.md#principled-implementation Literal paths remain individual inputs; wildcard paths use their fixed directory prefix and descend when remaining path components can contain matched files.
|
|
511
|
+
* @evidence contracts/common.md#clear-and-simple-design A single fixed-prefix calculation and descent decision produce the directory descriptor consumed by fingerprinting.
|
|
512
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Descent is not inferred solely from globstar, which would miss wildcard directory components followed by a filename.
|
|
513
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose documents literal paths, bare wildcards and wildcard-directory descent.
|
|
514
|
+
* @evidence contracts/performance.md#efficient-algorithms Pattern scanning is linear in pattern length and avoids directory traversal at selection time; a bare filename wildcard keeps the walk shallow.
|
|
515
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work This pure descriptor calculation has no cross-request producer or validation coordinator; publication owns reuse.
|
|
516
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The returned descriptor transfers to its caller and owns no handle or retained task.
|
|
517
|
+
* @evidence contracts/portability.md#os-neutral-implementation Both slash forms are recognized at the native-path boundary and relative roots resolve through Node path APIs.
|
|
405
518
|
*/
|
|
406
519
|
export function watchedBy(
|
|
407
520
|
pattern: string,
|
|
408
521
|
cwd: string,
|
|
409
522
|
): IArtifactDirectory | null {
|
|
410
523
|
if (pattern.search(GLOB_MAGIC) < 0) return null;
|
|
411
|
-
|
|
524
|
+
const magic = pattern.search(GLOB_MAGIC);
|
|
525
|
+
return {
|
|
526
|
+
path: globRoot(pattern, cwd),
|
|
527
|
+
recursive: pattern.includes("**") || /[/\\]/u.test(pattern.slice(magic)),
|
|
528
|
+
};
|
|
412
529
|
}
|
|
413
530
|
|
|
414
531
|
/**
|
|
@@ -458,37 +575,72 @@ function absolute(target: string, cwd: string): string {
|
|
|
458
575
|
/**
|
|
459
576
|
* The state of every input, as one comparable string.
|
|
460
577
|
*
|
|
461
|
-
* Files are
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
578
|
+
* Files are hashed by content so same-size edits with restored modification
|
|
579
|
+
* times remain observable. Directory membership is walked conservatively from
|
|
580
|
+
* declared roots; matching remains the sidecar's responsibility.
|
|
581
|
+
*
|
|
582
|
+
* @evidence contracts/common.md#principled-implementation The fingerprint includes input paths and regular-file bytes; sorted records make enumeration order irrelevant while missing paths retain an explicit marker.
|
|
583
|
+
* @evidence contracts/common.md#clear-and-simple-design File state and conservative directory traversal share one record representation before the final digest.
|
|
584
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Metadata equality, hidden-directory skipping and arbitrary depth limits do not stand in for declared input identity.
|
|
585
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose states content hashing, conservative membership and sidecar ownership of actual matching.
|
|
586
|
+
* @evidence contracts/performance.md#efficient-algorithms Cost is linear in watched regular-file bytes plus O(N log N) record sorting; broad recursive globs conservatively over-read and may invalidate unrelated edits.
|
|
587
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work This computes one validation identity; the session compares it with the publication snapshot and decides reuse.
|
|
588
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Synchronous reads close their descriptors internally, and the record array exists only for this computation; no historical fingerprint state is retained here.
|
|
589
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native stat, read and directory APIs handle platform paths; NUL-separated fields prevent filename ambiguity without shell serialization.
|
|
466
590
|
*/
|
|
467
591
|
export function fingerprintInputs(inputs: IArtifactInputs): string {
|
|
468
592
|
const parts: string[] = [];
|
|
469
|
-
|
|
593
|
+
const states = new Map<string, string>();
|
|
594
|
+
for (const file of inputs.files) parts.push(stateOf(file, states));
|
|
470
595
|
for (const directory of inputs.directories)
|
|
471
|
-
|
|
596
|
+
for (const entryToAppend of walkState(
|
|
597
|
+
directory.path,
|
|
598
|
+
directory.recursive,
|
|
599
|
+
states,
|
|
600
|
+
))
|
|
601
|
+
parts.push(entryToAppend);
|
|
472
602
|
parts.sort();
|
|
473
603
|
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
474
604
|
}
|
|
475
605
|
|
|
476
606
|
/**
|
|
477
|
-
* A
|
|
607
|
+
* A regular file's content identity, physical directory identity, or absence.
|
|
478
608
|
*
|
|
479
609
|
* The fields are joined on a character a path cannot contain. Separated by a
|
|
480
610
|
* space, a file literally named `a 1 2` states the same string as a one-byte
|
|
481
611
|
* file named `a`, and an edit to either would then read as no edit at all.
|
|
482
612
|
*/
|
|
483
|
-
function stateOf(file: string): string {
|
|
613
|
+
function stateOf(file: string, states: Map<string, string>): string {
|
|
614
|
+
const existing = states.get(file);
|
|
615
|
+
if (existing !== undefined) return existing;
|
|
484
616
|
try {
|
|
485
617
|
const stat = fs.statSync(file);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
618
|
+
const identity = stat.isFile()
|
|
619
|
+
? createHash("sha256").update(fs.readFileSync(file)).digest("hex")
|
|
620
|
+
: stat.isDirectory()
|
|
621
|
+
? ["directory", fs.realpathSync.native(file)].join(SEPARATOR)
|
|
622
|
+
: ["other", String(stat.size), String(stat.mtimeMs)].join(SEPARATOR);
|
|
623
|
+
const state = [file, identity].join(SEPARATOR);
|
|
624
|
+
states.set(file, state);
|
|
625
|
+
return state;
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (!missingPath(error)) throw error;
|
|
628
|
+
const state = [file, "absent"].join(SEPARATOR);
|
|
629
|
+
states.set(file, state);
|
|
630
|
+
return state;
|
|
489
631
|
}
|
|
490
632
|
}
|
|
491
633
|
|
|
634
|
+
/** Missing paths are inputs; unreadable existing inputs are failures. */
|
|
635
|
+
function missingPath(error: unknown): boolean {
|
|
636
|
+
return (
|
|
637
|
+
typeof error === "object" &&
|
|
638
|
+
error !== null &&
|
|
639
|
+
"code" in error &&
|
|
640
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
492
644
|
/**
|
|
493
645
|
* The character that joins fields a path could otherwise forge.
|
|
494
646
|
*
|
|
@@ -509,40 +661,67 @@ function globRoot(pattern: string, cwd: string): string {
|
|
|
509
661
|
const magic = pattern.search(GLOB_MAGIC);
|
|
510
662
|
const head = magic < 0 ? pattern : pattern.slice(0, magic);
|
|
511
663
|
const slash = Math.max(head.lastIndexOf("/"), head.lastIndexOf("\\"));
|
|
512
|
-
const root =
|
|
664
|
+
const root =
|
|
665
|
+
slash < 0
|
|
666
|
+
? ""
|
|
667
|
+
: head.slice(0, Math.max(slash, path.parse(head).root.length));
|
|
513
668
|
return root === "" ? cwd : absolute(root, cwd);
|
|
514
669
|
}
|
|
515
670
|
|
|
516
671
|
/**
|
|
517
672
|
* Every entry below `directory`, stated.
|
|
518
673
|
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
* directories are skipped below the declared root, which is what the glob that
|
|
523
|
-
* named it would have matched anyway.
|
|
674
|
+
* Descent follows the declared pattern rather than a guessed maximum depth or
|
|
675
|
+
* excluded directory name. Symlink directories are followed with a physical
|
|
676
|
+
* ancestor set so their declared contents are watched without creating cycles.
|
|
524
677
|
*
|
|
525
678
|
* A directory that does not exist states itself absent, which is what notices
|
|
526
679
|
* one being created.
|
|
527
680
|
*/
|
|
528
|
-
function walkState(
|
|
529
|
-
|
|
681
|
+
function walkState(
|
|
682
|
+
directory: string,
|
|
683
|
+
recursive: boolean,
|
|
684
|
+
inputStates: Map<string, string>,
|
|
685
|
+
ancestors: ReadonlySet<string> = new Set(),
|
|
686
|
+
): string[] {
|
|
530
687
|
let entries: fs.Dirent[];
|
|
688
|
+
let physical: string;
|
|
531
689
|
try {
|
|
690
|
+
physical = fs.realpathSync.native(directory);
|
|
691
|
+
if (ancestors.has(physical)) return [stateOf(directory, inputStates)];
|
|
532
692
|
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
533
|
-
} catch {
|
|
534
|
-
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (!missingPath(error)) throw error;
|
|
695
|
+
return [stateOf(directory, inputStates)];
|
|
535
696
|
}
|
|
697
|
+
const nextAncestors = new Set(ancestors);
|
|
698
|
+
nextAncestors.add(physical);
|
|
536
699
|
const states: string[] = [];
|
|
537
700
|
for (const entry of entries) {
|
|
538
701
|
const child = path.join(directory, entry.name);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
702
|
+
let directoryEntry = entry.isDirectory();
|
|
703
|
+
if (entry.isSymbolicLink()) {
|
|
704
|
+
try {
|
|
705
|
+
directoryEntry = fs.statSync(child).isDirectory();
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (!missingPath(error)) throw error;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (directoryEntry) {
|
|
711
|
+
if (!recursive) {
|
|
712
|
+
states.push(stateOf(child, inputStates));
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
for (const entryToAppend of walkState(
|
|
716
|
+
child,
|
|
717
|
+
recursive,
|
|
718
|
+
inputStates,
|
|
719
|
+
nextAncestors,
|
|
720
|
+
))
|
|
721
|
+
states.push(entryToAppend);
|
|
543
722
|
continue;
|
|
544
723
|
}
|
|
545
|
-
states.push(stateOf(child));
|
|
724
|
+
states.push(stateOf(child, inputStates));
|
|
546
725
|
}
|
|
547
726
|
return states;
|
|
548
727
|
}
|