feat(infospace): add L3 relation graph with VSM-aware triplets (S2.8)
Implements the L3 relation graph layer — a directed graph of (Subject,
Predicate, Object) triplets annotated with VSM channel codes and feedback
roles. Triplets are authored as markdown files under output/relations/,
parsed into RelationMeta dataclasses, and analysed with networkx.
New modules:
- markitect/infospace/relation_models.py — RelationMeta dataclass +
RELATION_TYPES controlled vocabulary (15 relation classes → VSM codes)
- markitect/infospace/relation_parser.py — parse_relation_file() and
parse_relations_directory()
New schema: examples/infospace-with-history/schemas/relation-schema-v1.0.md
— file naming convention, required sections, controlled vocabulary table
15 seed relation files covering the three core WoN feedback loops:
- Capital Accumulation loop (positive reinforcement, S1/S3)
- Market Price Balancing loop (negative feedback, S2/S3)
- Market Extent mutual dependency (S1/S2)
Plus structural relations: wages regulation, rent residual, price
decomposition, invisible hand coordination
CLI: markitect infospace relations [--entity SLUG] [--vsm FILTER]
[--loops] [--stats]
- Builds directed graph from parsed files
- Detects feedback loops via nx.simple_cycles()
- 6 loops found from 15 seed relations (3 intended + 3 emergent)
- --stats aggregates by VSM system code (strips parentheticals)
Config: InfospaceConfig gains relations_dir (default output/relations)
infospace.yaml: schemas.relation references relation-schema-v1.0.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ schemas:
|
|||||||
entity: schemas/economic-entity-schema-v1.0.md
|
entity: schemas/economic-entity-schema-v1.0.md
|
||||||
mapping: schemas/vsm-mapping-schema-v1.0.md
|
mapping: schemas/vsm-mapping-schema-v1.0.md
|
||||||
analysis: schemas/chapter-analysis-schema-v1.0.md
|
analysis: schemas/chapter-analysis-schema-v1.0.md
|
||||||
|
relation: schemas/relation-schema-v1.0.md
|
||||||
|
|
||||||
competency_questions: |
|
competency_questions: |
|
||||||
1. How does Smith's division of labour map to VSM System 1 operations?
|
1. How does Smith's division of labour map to VSM System 1 operations?
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Accumulation of Stock — enables — Division of Labour
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Accumulation of Stock
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
enables
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Division of Labour
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
enables
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S3 → S1 (capital stock managed at S3 deploys productive capacity at S1)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 6: "As soon as stock has accumulated in the hands of
|
||||||
|
particular persons, some of them will naturally employ it in setting to work
|
||||||
|
industrious people, whom they will supply with materials and subsistence."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Capital Accumulation loop (positive/reinforcing):**
|
||||||
|
Accumulation of Stock → enables → Division of Labour → increases →
|
||||||
|
Productive Powers of Labour → raises → Profits of Stock → funds →
|
||||||
|
Accumulation of Stock.
|
||||||
|
|
||||||
|
This edge (enables) is the initiating link: accumulated capital is the
|
||||||
|
prerequisite for organising specialised production. Without prior accumulation
|
||||||
|
there is no employer to assign and coordinate different tasks.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Accumulation of Stock — increases supply that depresses — Market Price of Commodities
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Accumulation of Stock
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
increases supply, depressing
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Market Price of Commodities
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
regulates
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S3 → S2 (capital deployment increases supply, correcting price signal)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 7: "When the quantity brought to market exceeds the
|
||||||
|
effectual demand, it cannot be all sold to those who are willing to pay
|
||||||
|
the whole value of the rent, wages and profit, which must be paid in
|
||||||
|
order to bring it thither. Some part must be sold to those who are
|
||||||
|
willing to pay less, and the low price which they give for it must
|
||||||
|
reduce the price of the whole."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Market Price Balancing loop (negative/balancing):**
|
||||||
|
Market Price above Natural Price → attracts → Capital inflow →
|
||||||
|
increases supply → depresses → Market Price back toward Natural Price.
|
||||||
|
|
||||||
|
This edge (increases supply, depressing) closes the balancing loop:
|
||||||
|
directed capital raises output until oversupply brings price back down.
|
||||||
|
This is the mechanism behind Smith's claim that natural price acts as
|
||||||
|
the "central price, to which the prices of all commodities are
|
||||||
|
continually gravitating."
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Division of Labour — constrains — Market Extent
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Division of Labour
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
limited by
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Market Extent
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
constrains
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S2 ← S1 (the coordination reach of markets bounds the scope of operational specialisation)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 3: "As it is the power of exchanging that gives occasion
|
||||||
|
to the division of labour, so the extent of this division must always be
|
||||||
|
limited by the extent of that power, or, in other words, by the extent
|
||||||
|
of the market."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Market Expansion loop (positive/reinforcing):**
|
||||||
|
Market Extent → enables greater Division of Labour → raises Productive
|
||||||
|
Powers of Labour → generates surplus → expands Trade → expands
|
||||||
|
Market Extent.
|
||||||
|
|
||||||
|
This edge (limited by) is the structural constraint that gives the loop
|
||||||
|
its character: the level of specialisation that can be sustained depends
|
||||||
|
on the size of the market that can absorb the specialised output.
|
||||||
|
Smith's famous opening of Chapter III states this with unusual directness.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Division of Labour — increases — Productive Powers of Labour
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Division of Labour
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
increases
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Productive Powers of Labour
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
enables
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S1 → S1 (operational specialisation raises operational output per unit)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 1: "The greatest improvement in the productive powers of
|
||||||
|
labour, and the greater part of the skill, dexterity, and judgment with
|
||||||
|
which it is anywhere directed, or applied, seem to have been the effects
|
||||||
|
of the division of labour."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Capital Accumulation loop (positive/reinforcing):**
|
||||||
|
Accumulation of Stock → enables → Division of Labour → increases →
|
||||||
|
Productive Powers of Labour → raises → Profits of Stock → funds →
|
||||||
|
Accumulation of Stock.
|
||||||
|
|
||||||
|
This edge (increases) is the productivity mechanism: specialisation
|
||||||
|
improves dexterity, saves time between tasks, and facilitates the
|
||||||
|
invention of machinery — three distinct channels that all raise output
|
||||||
|
per worker.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Invisible Hand Mechanism — coordinates — Accumulation of Stock
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Invisible Hand Mechanism
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
coordinates (directs capital allocation without central direction)
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Accumulation of Stock
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
coordinates
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S4 → S3 (distributed environmental intelligence shapes capital allocation decisions)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book IV, Chapter 2: "He generally, indeed, neither intends to promote the
|
||||||
|
public interest, nor knows how much he is promoting it... he intends only
|
||||||
|
his own security; and by directing that industry in a manner whose produce
|
||||||
|
may be of the greatest value, he intends only his own gain, and he is in
|
||||||
|
this, as in many other cases, led by an invisible hand to promote an end
|
||||||
|
which was no part of his intention."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
The Invisible Hand Mechanism is the S4 intelligence function that makes
|
||||||
|
the Market Price Balancing loop self-organising. Individual capitalists
|
||||||
|
seeking private profit collectively direct accumulation to where returns
|
||||||
|
are highest, which (through the market price balancing loop) also serves
|
||||||
|
as a resource allocation mechanism. This relation explains why the system
|
||||||
|
does not require a planner — the S4 function is distributed across all
|
||||||
|
capital owners simultaneously.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Market Extent — enables — Division of Labour
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Market Extent
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
enables (by providing demand for specialised output)
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Division of Labour
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
enables
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S2 → S1 (coordination reach of markets enables operational specialisation)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 3: "When the market is very small, no person can have
|
||||||
|
any encouragement to dedicate himself entirely to one employment, for
|
||||||
|
want of the power to exchange all that surplus part of the produce of his
|
||||||
|
own labour, which is over and above his own consumption, for such parts
|
||||||
|
of the produce of other men's labour as he has occasion for."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Market Expansion loop (positive/reinforcing):**
|
||||||
|
Market Extent → enables → Division of Labour → raises Productive Powers
|
||||||
|
of Labour → generates surplus → expands Trade → expands Market Extent.
|
||||||
|
|
||||||
|
Together with the constrains edge (Division of Labour ← Market Extent),
|
||||||
|
this pair forms the mutual dependency at the heart of Book I: markets
|
||||||
|
enable specialisation, and specialisation generates the surplus that
|
||||||
|
expands markets.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Market Price of Commodities — attracts — Accumulation of Stock
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Market Price of Commodities
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
attracts (when above natural price)
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Accumulation of Stock
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
coordinates
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S2 → S3 (price signal coordinates capital reallocation)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 7: "When the quantity brought to market falls short of the
|
||||||
|
effectual demand, all those who are willing to pay the whole natural price
|
||||||
|
cannot be supplied. Some of them will be willing to give more. A competition
|
||||||
|
will immediately begin among them, and the market price will rise more or
|
||||||
|
less above the natural price."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Market Price Balancing loop (negative/balancing):**
|
||||||
|
Market Price above Natural Price → attracts → Capital inflow (Accumulation
|
||||||
|
directed to this market) → increases Supply → depresses → Market Price
|
||||||
|
back toward Natural Price.
|
||||||
|
|
||||||
|
This edge (attracts) is the signal-response link: a market price above
|
||||||
|
natural price is a profit opportunity that draws capital from other uses.
|
||||||
|
Smith's key insight is that this signal is self-cancelling — it eliminates
|
||||||
|
the very excess that created it.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Market Price of Commodities — decomposes into — Profits of Stock
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Market Price of Commodities
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
decomposes into (profit as component)
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Profits of Stock
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
produces
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S2 → S3 (price signal resolves into return on capital, which S3 manages)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 6: "In every society the price of every commodity
|
||||||
|
finally resolves itself into some one or other, or all of those three
|
||||||
|
parts; and in every improved society, all three enter more or less,
|
||||||
|
as component parts, into the price of the far greater part of commodities."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
Part of the **Three-Component Decomposition** (see also wages and rent
|
||||||
|
relations). This edge links the market price coordination signal (S2)
|
||||||
|
to the capital management outcome (S3), establishing profit as the
|
||||||
|
mechanism by which market performance translates into the fund for
|
||||||
|
future accumulation. It is the bridge between the Market Price Balancing
|
||||||
|
loop and the Capital Accumulation loop.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Market Price of Commodities — decomposes into — Wages of Labour
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Market Price of Commodities
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
decomposes into (wages as component)
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Wages of Labour
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
produces
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S2 → S2 (price as coordination signal resolves into its component coordination signals)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 6: "In every society the price of every commodity
|
||||||
|
finally resolves itself into some one or other, or all of those three
|
||||||
|
parts [wages, profit, rent]; and in every improved society, all three
|
||||||
|
enter more or less, as component parts, into the price of the far greater
|
||||||
|
part of commodities."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
This is one of the three price-decomposition relations (alongside market
|
||||||
|
price → profits of stock, and market price → rent of land). Together
|
||||||
|
they form the **Three-Component Decomposition**: the price system
|
||||||
|
connects the coordination layer (S2) back to the distributional outcomes
|
||||||
|
for all three factor markets. The decomposition is the analytical
|
||||||
|
foundation from which Books I and II derive their structure.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Natural Price as Central Price — centres — Market Price of Commodities
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Natural Price as Central Price
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
centres
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Market Price of Commodities
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
coordinates
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S2 (natural price is the attractor in the S2 coordination field)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 7: "The natural price, therefore, is, as it were, the
|
||||||
|
central price, to which the prices of all commodities are continually
|
||||||
|
gravitating. Different accidents may sometimes keep them suspended a good
|
||||||
|
deal above it, and sometimes force them down even somewhat below it."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
This relation is the attractor definition for the Market Price Balancing
|
||||||
|
loop — natural price is the equilibrium that both loops (attraction of
|
||||||
|
capital when above, flight of capital when below) converge on. It is the
|
||||||
|
S2 coordination reference point around which oscillation is suppressed.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Productive Powers of Labour — raises — Profits of Stock
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Productive Powers of Labour
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
raises
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Profits of Stock
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
produces
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S1 → S3 (operational productivity surplus allocated as profit at capital management level)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 6: "The value which the workmen add to the materials
|
||||||
|
resolves itself into two parts, of which the one pays their wages, the
|
||||||
|
other the profits of their employer upon the whole stock of materials
|
||||||
|
and wages which he advanced."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Capital Accumulation loop (positive/reinforcing):**
|
||||||
|
Accumulation of Stock → enables → Division of Labour → increases →
|
||||||
|
Productive Powers of Labour → raises → Profits of Stock → funds →
|
||||||
|
Accumulation of Stock.
|
||||||
|
|
||||||
|
This edge (raises) is the surplus-extraction link: the productivity gain
|
||||||
|
produced by specialisation shows up as profit over and above wage costs,
|
||||||
|
generating the fund from which further accumulation occurs.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Profits of Stock — funds — Accumulation of Stock
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Profits of Stock
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
funds
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Accumulation of Stock
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
produces
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S3 → S3 (profit retained by capital managers re-enters the stock managed at S3)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book II, Chapter 3: "Whatever a person saves from his revenue he adds to
|
||||||
|
his capital, and either employs it himself in maintaining an additional
|
||||||
|
number of productive hands, or enables some other person to do so, by
|
||||||
|
lending it to him for an interest."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Capital Accumulation loop (positive/reinforcing):**
|
||||||
|
Accumulation of Stock → enables → Division of Labour → increases →
|
||||||
|
Productive Powers of Labour → raises → Profits of Stock → funds →
|
||||||
|
Accumulation of Stock.
|
||||||
|
|
||||||
|
This edge (funds) closes the loop: profit not consumed becomes saving,
|
||||||
|
and saving becomes the new stock that enables the next round of division
|
||||||
|
of labour. This is why Smith regards frugality as a public virtue — it
|
||||||
|
fuels productive capacity rather than dissipating it.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Rent of Land — regulated by — Market Price of Commodities
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Rent of Land
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
regulated by (rent is a residual after wages and profit are paid)
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Market Price of Commodities
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
is regulated by
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S2 → S3 (price signal determines what residual rent the land can command)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 11: "Rent, it is to be observed, therefore, enters into
|
||||||
|
the composition of the price of commodities in a different manner from
|
||||||
|
wages and profit. High or low wages and profit are the causes of high or
|
||||||
|
low price; high or low rent is the effect of it."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
This relation establishes rent as a **residual** in the price decomposition
|
||||||
|
rather than a cost that determines price. It places Rent of Land in a
|
||||||
|
structurally subordinate position to the Market Price mechanism — rent
|
||||||
|
takes what is left after labour and capital have been paid, rather than
|
||||||
|
competing with them as a co-equal cost component. This is Smith's
|
||||||
|
sharpest break from the physiocrats.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Wages of Labour — regulated by — Accumulation of Stock
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Wages of Labour
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
regulated by (set by the demand for labour, which tracks capital growth)
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Accumulation of Stock
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
is regulated by
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S3 → S2 (capital stock sets demand for labour, which sets the wage coordination signal)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 8: "The demand for those who live by wages, therefore,
|
||||||
|
necessarily increases with the increase of the revenue and stock of every
|
||||||
|
country, and cannot possibly increase without it. The increase of revenue
|
||||||
|
and stock is the increase of national wealth."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
**Wages Expansion loop (positive/reinforcing):**
|
||||||
|
Accumulation of Stock → raises demand for labour → raises Wages of Labour
|
||||||
|
→ raises consumer capacity → stimulates Productive Powers of Labour →
|
||||||
|
generates surplus → funds Accumulation of Stock.
|
||||||
|
|
||||||
|
This edge (regulated by) establishes the first link: the wage rate
|
||||||
|
responds to the growth of capital stock, not to the current stock level
|
||||||
|
directly. Rapid accumulation raises wages; stationary stock does not.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Wages of Labour — regulated by — Profits of Stock
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
|
||||||
|
Wages of Labour
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
|
||||||
|
regulated by (bounded above by)
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
Profits of Stock
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
|
||||||
|
is regulated by
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
|
||||||
|
S3 → S2 (capital management sets the upper bound on the coordination signal)
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Book I, Chapter 8: "In order to bring up a family, the labour of the
|
||||||
|
husband and wife together must, even in the lowest species of common
|
||||||
|
labour, be able to earn something more than what is precisely necessary
|
||||||
|
for their own maintenance; but in what is commonly called common labour
|
||||||
|
it is rare that a single labourer earns much more... The interests of
|
||||||
|
the two parties are by no means the same. The workmen desire to get as
|
||||||
|
much, the masters to give as little as possible."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
|
||||||
|
This relation establishes the distributional constraint that bounds
|
||||||
|
the Wages loop. Profits of Stock and Wages of Labour are in permanent
|
||||||
|
tension: every increase in one (holding productivity constant) reduces
|
||||||
|
the other. This is Smith's clearest statement of class interest as a
|
||||||
|
structural feature of market economies.
|
||||||
139
examples/infospace-with-history/schemas/relation-schema-v1.0.md
Normal file
139
examples/infospace-with-history/schemas/relation-schema-v1.0.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Relation Triplet Schema — v1.0
|
||||||
|
|
||||||
|
Each file in `output/relations/` captures a single directed relation between
|
||||||
|
two entities from the L1 entity collection. Relations are the edges of the
|
||||||
|
L3 relation graph.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Required Sections
|
||||||
|
|
||||||
|
### H1 — Relation title
|
||||||
|
|
||||||
|
Format: `# Subject — predicate phrase — Object`
|
||||||
|
|
||||||
|
Example: `# Division of Labour — limited by — Market Extent`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Subject
|
||||||
|
|
||||||
|
The entity that is the **source** of the relation — the thing doing the
|
||||||
|
enabling, constraining, producing, etc.
|
||||||
|
|
||||||
|
Use the entity's exact title as it appears in its L1 entity file (H1 heading).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Predicate
|
||||||
|
|
||||||
|
A short phrase from the **controlled relation vocabulary** (see below), or
|
||||||
|
a natural language variant that maps to one of those classes.
|
||||||
|
|
||||||
|
Examples: `limited by`, `enables`, `is regulated by`, `produces`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Object
|
||||||
|
|
||||||
|
The entity that is the **target** of the relation — the thing being enabled,
|
||||||
|
constrained, regulated, etc.
|
||||||
|
|
||||||
|
Use the entity's exact title as it appears in its L1 entity file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Relation Type
|
||||||
|
|
||||||
|
The **semantic class** of the predicate, drawn from the controlled vocabulary:
|
||||||
|
|
||||||
|
| Relation Type | Meaning | Default VSM Channel |
|
||||||
|
|---|---|---|
|
||||||
|
| `enables` | Subject makes object possible or more effective | S1 → S1 |
|
||||||
|
| `constrains` | Subject limits or bounds the object | S1 ← S1 |
|
||||||
|
| `regulates` | Subject actively governs the object | S3 → S1 |
|
||||||
|
| `is regulated by` | Object governs the subject | S1 ← S3 |
|
||||||
|
| `coordinates` | Subject reduces oscillation or misalignment | S2 |
|
||||||
|
| `produces` | Subject generates the object as output | S1 |
|
||||||
|
| `consumes` | Subject depletes the object | S1 |
|
||||||
|
| `monitors` | Subject observes and reports on object | S3* |
|
||||||
|
| `audits` | Subject verifies compliance of object | S3* |
|
||||||
|
| `adapts to` | Subject adjusts in response to object | S4 |
|
||||||
|
| `anticipates` | Subject predicts or models the object | S4 |
|
||||||
|
| `defines` | Subject sets the identity or rules of object | S5 → any |
|
||||||
|
| `is defined by` | Object sets the identity or rules of subject | any ← S5 |
|
||||||
|
| `contradicts` | Subject is in direct logical conflict with object | any |
|
||||||
|
| `tensions with` | Subject and object exist in productive tension | any |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VSM Channel
|
||||||
|
|
||||||
|
The Viable System Model systems involved in this relation, with direction.
|
||||||
|
|
||||||
|
Format: `S<n> → S<m>` or `S<n>` if the relation is within a single system.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `S1 → S2` — operation drives a coordination signal
|
||||||
|
- `S3 → S1` — management regulates operations
|
||||||
|
- `S2` — pure coordination within S2
|
||||||
|
- `S4 → S5` — intelligence informing policy
|
||||||
|
|
||||||
|
Use `S3*` for audit loops (Beer's algedonic channel).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
|
||||||
|
Direct textual evidence from Adam Smith's text. Preferred format:
|
||||||
|
|
||||||
|
```
|
||||||
|
Book I, Chapter 3: "The division of labour is limited by the extent of the market."
|
||||||
|
```
|
||||||
|
|
||||||
|
Use short quotes when available. Chapter reference alone is acceptable when
|
||||||
|
no single sentence captures the relation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Feedback Role (optional)
|
||||||
|
|
||||||
|
If this relation is part of a named feedback loop, describe its role here.
|
||||||
|
Name the loop and state what the relation contributes.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
Part of the Market Expansion loop: larger market → more specialisation
|
||||||
|
→ higher productivity → greater surplus → expanded trade → larger market.
|
||||||
|
This edge (limited by) is the constraining link that turns the loop balancing
|
||||||
|
when market extent cannot grow further.
|
||||||
|
```
|
||||||
|
|
||||||
|
Omit or leave empty if the relation is not part of a known feedback loop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Naming
|
||||||
|
|
||||||
|
Use the pattern: `<subject-slug>--<relation-type>--<object-slug>.md`
|
||||||
|
|
||||||
|
Where slugs are derived from entity titles using the same slugification as
|
||||||
|
entity files (lowercase, spaces and punctuation replaced with underscores).
|
||||||
|
|
||||||
|
Example: `division_of_labour--constrains--market_extent.md`
|
||||||
|
|
||||||
|
If two entities have the same relation type between them (unusual), append
|
||||||
|
a numeric suffix: `...-2.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Completeness Criteria
|
||||||
|
|
||||||
|
A relation file is **complete** if:
|
||||||
|
- All four required sections are present and non-empty
|
||||||
|
- The Relation Type is from the controlled vocabulary
|
||||||
|
- The VSM Channel is a valid VSM system designation
|
||||||
|
- Evidence references a specific Book and Chapter
|
||||||
|
|
||||||
|
A relation file is **acceptable** if:
|
||||||
|
- Required sections present but Evidence is missing (mark as `(unverified)`)
|
||||||
@@ -299,6 +299,126 @@ def eval_summary(config_path: Optional[str], update_metrics: bool):
|
|||||||
click.echo(f"\nUpdated metrics.yaml: per_entity_mean = {mean_overall:.4f}")
|
click.echo(f"\nUpdated metrics.yaml: per_entity_mean = {mean_overall:.4f}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── relations ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@infospace_commands.command()
|
||||||
|
@click.option("--config", "config_path", default=None, help="Path to infospace.yaml.")
|
||||||
|
@click.option("--entity", "entity_slug", default=None,
|
||||||
|
help="Show only relations involving this entity slug.")
|
||||||
|
@click.option("--vsm", "vsm_filter", default=None,
|
||||||
|
help="Show only relations whose VSM channel contains this string (e.g. S2, S3).")
|
||||||
|
@click.option("--loops", "loops_only", is_flag=True, default=False,
|
||||||
|
help="Show only feedback loops (cycles in the relation graph).")
|
||||||
|
@click.option("--stats", "stats_only", is_flag=True, default=False,
|
||||||
|
help="Show aggregate statistics only, no individual relations.")
|
||||||
|
def relations(config_path: Optional[str], entity_slug: Optional[str],
|
||||||
|
vsm_filter: Optional[str], loops_only: bool, stats_only: bool):
|
||||||
|
"""Show the L3 relation graph — triplets, feedback loops, and VSM channels."""
|
||||||
|
cfg, cfg_path = _load_config_or_exit(config_path)
|
||||||
|
root = cfg_path.parent
|
||||||
|
|
||||||
|
from markitect.infospace.relation_parser import parse_relations_directory
|
||||||
|
|
||||||
|
relations_dir = root / cfg.relations_dir
|
||||||
|
if not relations_dir.is_dir():
|
||||||
|
click.echo("No relations directory found. Create output/relations/ and add relation files.")
|
||||||
|
return
|
||||||
|
|
||||||
|
all_relations = parse_relations_directory(relations_dir)
|
||||||
|
if not all_relations:
|
||||||
|
click.echo("No relation files found in " + str(relations_dir))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build directed graph for cycle detection
|
||||||
|
try:
|
||||||
|
import networkx as nx
|
||||||
|
G = nx.DiGraph()
|
||||||
|
for r in all_relations:
|
||||||
|
G.add_edge(r.subject_slug, r.object_slug,
|
||||||
|
predicate=r.predicate,
|
||||||
|
relation_type=r.relation_type,
|
||||||
|
vsm_channel=r.vsm_channel,
|
||||||
|
slug=r.slug)
|
||||||
|
except ImportError:
|
||||||
|
G = None
|
||||||
|
|
||||||
|
# Find feedback loops
|
||||||
|
loops = []
|
||||||
|
if G is not None:
|
||||||
|
try:
|
||||||
|
loops = list(nx.simple_cycles(G))
|
||||||
|
except Exception:
|
||||||
|
loops = []
|
||||||
|
|
||||||
|
# Stats summary
|
||||||
|
import re as _re
|
||||||
|
|
||||||
|
def _vsm_code(channel: str) -> str:
|
||||||
|
"""Strip parenthetical description, returning just the system code (e.g. 'S3 → S1')."""
|
||||||
|
return _re.sub(r'\s*\(.*', '', channel).strip() or channel
|
||||||
|
|
||||||
|
n = len(all_relations)
|
||||||
|
vsm_counts: dict = {}
|
||||||
|
type_counts: dict = {}
|
||||||
|
for r in all_relations:
|
||||||
|
vsm_counts[_vsm_code(r.vsm_channel)] = vsm_counts.get(_vsm_code(r.vsm_channel), 0) + 1
|
||||||
|
type_counts[r.relation_type] = type_counts.get(r.relation_type, 0) + 1
|
||||||
|
|
||||||
|
click.echo(f"Relation graph — {n} relations")
|
||||||
|
if G is not None:
|
||||||
|
click.echo(f" Entities in graph: {G.number_of_nodes()}")
|
||||||
|
click.echo(f" Feedback loops: {len(loops)}")
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
if stats_only:
|
||||||
|
click.echo("Relation types:")
|
||||||
|
for rt, count in sorted(type_counts.items(), key=lambda x: -x[1]):
|
||||||
|
click.echo(f" {rt:<25} {count:>4}")
|
||||||
|
click.echo()
|
||||||
|
click.echo("VSM channels:")
|
||||||
|
for ch, count in sorted(vsm_counts.items(), key=lambda x: -x[1]):
|
||||||
|
click.echo(f" {ch:<20} {count:>4}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Feedback loops section
|
||||||
|
if loops or loops_only:
|
||||||
|
if loops:
|
||||||
|
click.echo(f"Feedback loops ({len(loops)}):")
|
||||||
|
for i, cycle in enumerate(loops, 1):
|
||||||
|
click.echo(f" Loop {i}: {' → '.join(cycle)} → {cycle[0]}")
|
||||||
|
click.echo()
|
||||||
|
elif loops_only:
|
||||||
|
click.echo("No feedback loops detected in current relation set.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if loops_only:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Filter relations
|
||||||
|
filtered = all_relations
|
||||||
|
if entity_slug:
|
||||||
|
filtered = [r for r in filtered
|
||||||
|
if entity_slug in (r.subject_slug, r.object_slug)]
|
||||||
|
if not filtered:
|
||||||
|
click.echo(f"No relations found involving '{entity_slug}'.")
|
||||||
|
return
|
||||||
|
if vsm_filter:
|
||||||
|
filtered = [r for r in filtered if vsm_filter in r.vsm_channel]
|
||||||
|
if not filtered:
|
||||||
|
click.echo(f"No relations with VSM channel containing '{vsm_filter}'.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Display relations
|
||||||
|
click.echo(f"{'Subject':<35} {'Predicate':<30} {'Object':<35} {'VSM'}")
|
||||||
|
click.echo("-" * 110)
|
||||||
|
for r in filtered:
|
||||||
|
subj = r.subject[:33] + ".." if len(r.subject) > 35 else r.subject
|
||||||
|
obj = r.object[:33] + ".." if len(r.object) > 35 else r.object
|
||||||
|
pred = r.predicate[:28] + ".." if len(r.predicate) > 30 else r.predicate
|
||||||
|
click.echo(f"{subj:<35} {pred:<30} {obj:<35} {r.vsm_channel}")
|
||||||
|
|
||||||
|
|
||||||
# ── viability ────────────────────────────────────────────────────────
|
# ── viability ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ class InfospaceConfig:
|
|||||||
entities_dir: str = "output/entities"
|
entities_dir: str = "output/entities"
|
||||||
evaluations_dir: str = "output/evaluations"
|
evaluations_dir: str = "output/evaluations"
|
||||||
metrics_dir: str = "output/metrics"
|
metrics_dir: str = "output/metrics"
|
||||||
|
relations_dir: str = "output/relations"
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
d: Dict[str, Any] = {"topic": self.topic.to_dict()}
|
d: Dict[str, Any] = {"topic": self.topic.to_dict()}
|
||||||
@@ -276,6 +277,8 @@ class InfospaceConfig:
|
|||||||
d["evaluations_dir"] = self.evaluations_dir
|
d["evaluations_dir"] = self.evaluations_dir
|
||||||
if self.metrics_dir != "output/metrics":
|
if self.metrics_dir != "output/metrics":
|
||||||
d["metrics_dir"] = self.metrics_dir
|
d["metrics_dir"] = self.metrics_dir
|
||||||
|
if self.relations_dir != "output/relations":
|
||||||
|
d["relations_dir"] = self.relations_dir
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -301,6 +304,7 @@ class InfospaceConfig:
|
|||||||
entities_dir=data.get("entities_dir", "output/entities"),
|
entities_dir=data.get("entities_dir", "output/entities"),
|
||||||
evaluations_dir=data.get("evaluations_dir", "output/evaluations"),
|
evaluations_dir=data.get("evaluations_dir", "output/evaluations"),
|
||||||
metrics_dir=data.get("metrics_dir", "output/metrics"),
|
metrics_dir=data.get("metrics_dir", "output/metrics"),
|
||||||
|
relations_dir=data.get("relations_dir", "output/relations"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
72
markitect/infospace/relation_models.py
Normal file
72
markitect/infospace/relation_models.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Data models for L3 relation triplets.
|
||||||
|
|
||||||
|
A relation triplet is the fundamental unit of the relation graph:
|
||||||
|
Subject --[Predicate]--> Object
|
||||||
|
|
||||||
|
Each triplet is stored as a markdown file in ``output/relations/``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
# Controlled relation vocabulary — maps semantic class to VSM channel
|
||||||
|
RELATION_TYPES = {
|
||||||
|
"enables": "S1 → S1",
|
||||||
|
"constrains": "S1 ← S1",
|
||||||
|
"regulates": "S3 → S1",
|
||||||
|
"is regulated by": "S1 ← S3",
|
||||||
|
"coordinates": "S2",
|
||||||
|
"produces": "S1",
|
||||||
|
"consumes": "S1",
|
||||||
|
"monitors": "S3*",
|
||||||
|
"audits": "S3*",
|
||||||
|
"adapts to": "S4",
|
||||||
|
"anticipates": "S4",
|
||||||
|
"defines": "S5 → any",
|
||||||
|
"is defined by": "any ← S5",
|
||||||
|
"contradicts": "any",
|
||||||
|
"tensions with": "any",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RelationMeta:
|
||||||
|
"""Structured metadata for a single relation triplet.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
slug: Unique identifier, e.g.
|
||||||
|
``division_of_labour--constrains--market_extent``
|
||||||
|
subject: Human-readable title of the subject entity
|
||||||
|
subject_slug: Slug of the subject entity (links to L1)
|
||||||
|
predicate: Human-readable predicate phrase, e.g. "limited by"
|
||||||
|
object: Human-readable title of the object entity
|
||||||
|
object_slug: Slug of the object entity (links to L1)
|
||||||
|
relation_type: Semantic class from the controlled vocabulary
|
||||||
|
vsm_channel: VSM systems involved, e.g. "S1 → S2"
|
||||||
|
evidence: Source text quote or chapter reference
|
||||||
|
feedback_role: Description of role in a feedback loop (if any)
|
||||||
|
source_path: Absolute path to the ``.md`` file
|
||||||
|
"""
|
||||||
|
|
||||||
|
slug: str
|
||||||
|
subject: str
|
||||||
|
subject_slug: str
|
||||||
|
predicate: str
|
||||||
|
object: str
|
||||||
|
object_slug: str
|
||||||
|
relation_type: str
|
||||||
|
vsm_channel: str
|
||||||
|
evidence: str = ""
|
||||||
|
feedback_role: str = ""
|
||||||
|
source_path: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_feedback_member(self) -> bool:
|
||||||
|
"""True if this relation participates in a named feedback loop."""
|
||||||
|
return bool(self.feedback_role.strip())
|
||||||
|
|
||||||
|
def edge(self) -> tuple:
|
||||||
|
"""Return a (subject_slug, object_slug, predicate) edge tuple."""
|
||||||
|
return (self.subject_slug, self.object_slug, self.predicate)
|
||||||
137
markitect/infospace/relation_parser.py
Normal file
137
markitect/infospace/relation_parser.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
Relation triplet parser.
|
||||||
|
|
||||||
|
Reads structured :class:`RelationMeta` objects from relation markdown
|
||||||
|
files in ``output/relations/``.
|
||||||
|
|
||||||
|
File format::
|
||||||
|
|
||||||
|
# Subject — predicate — Object
|
||||||
|
|
||||||
|
## Subject
|
||||||
|
Subject Entity Title
|
||||||
|
|
||||||
|
## Predicate
|
||||||
|
predicate phrase
|
||||||
|
|
||||||
|
## Object
|
||||||
|
Object Entity Title
|
||||||
|
|
||||||
|
## Relation Type
|
||||||
|
constrains
|
||||||
|
|
||||||
|
## VSM Channel
|
||||||
|
S1 → S2
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
Book I, Chapter 3: "..."
|
||||||
|
|
||||||
|
## Feedback Role
|
||||||
|
Part of the Market Expansion loop: ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Sequence
|
||||||
|
|
||||||
|
from markitect.core.parser import parse_markdown_to_ast
|
||||||
|
from markitect.core.section_tree import (
|
||||||
|
build_section_tree,
|
||||||
|
extract_section_text,
|
||||||
|
slugify,
|
||||||
|
)
|
||||||
|
from .relation_models import RelationMeta
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_h2(tree_root: dict, slug: str) -> Optional[dict]:
|
||||||
|
for child in tree_root.get("children", []):
|
||||||
|
if child["level"] == 2 and child["slug"] == slug:
|
||||||
|
return child
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _section_text(root: dict, slug: str) -> str:
|
||||||
|
node = _find_h2(root, slug)
|
||||||
|
return extract_section_text(node).strip() if node else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _slug_from_title(title: str) -> str:
|
||||||
|
"""Convert entity title to slug (same as slugify used in entity_parser)."""
|
||||||
|
return slugify(title)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_relation_file(path: Path) -> RelationMeta:
|
||||||
|
"""Parse a single relation markdown file into :class:`RelationMeta`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If required sections are missing.
|
||||||
|
"""
|
||||||
|
content = path.read_text(encoding="utf-8")
|
||||||
|
tokens = parse_markdown_to_ast(content)
|
||||||
|
tree = build_section_tree(tokens)
|
||||||
|
|
||||||
|
# Find H1
|
||||||
|
h1 = next(
|
||||||
|
(c for c in tree["children"] if c["level"] == 1),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if h1 is None:
|
||||||
|
raise ValueError(f"No H1 heading in {path}")
|
||||||
|
|
||||||
|
root = h1
|
||||||
|
|
||||||
|
subject = _section_text(root, "subject")
|
||||||
|
predicate = _section_text(root, "predicate")
|
||||||
|
obj = _section_text(root, "object")
|
||||||
|
relation_type = _section_text(root, "relation_type")
|
||||||
|
vsm_channel = _section_text(root, "vsm_channel")
|
||||||
|
evidence = _section_text(root, "evidence")
|
||||||
|
feedback_role = _section_text(root, "feedback_role")
|
||||||
|
|
||||||
|
if not subject:
|
||||||
|
raise ValueError(f"Missing ## Subject in {path}")
|
||||||
|
if not predicate:
|
||||||
|
raise ValueError(f"Missing ## Predicate in {path}")
|
||||||
|
if not obj:
|
||||||
|
raise ValueError(f"Missing ## Object in {path}")
|
||||||
|
|
||||||
|
subject_slug = _slug_from_title(subject)
|
||||||
|
object_slug = _slug_from_title(obj)
|
||||||
|
|
||||||
|
# Derive canonical slug from file stem
|
||||||
|
slug = path.stem
|
||||||
|
|
||||||
|
return RelationMeta(
|
||||||
|
slug=slug,
|
||||||
|
subject=subject,
|
||||||
|
subject_slug=subject_slug,
|
||||||
|
predicate=predicate,
|
||||||
|
object=obj,
|
||||||
|
object_slug=object_slug,
|
||||||
|
relation_type=relation_type,
|
||||||
|
vsm_channel=vsm_channel,
|
||||||
|
evidence=evidence,
|
||||||
|
feedback_role=feedback_role,
|
||||||
|
source_path=str(path),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_relations_directory(
|
||||||
|
directory: Path,
|
||||||
|
) -> List[RelationMeta]:
|
||||||
|
"""Parse all relation files in *directory*.
|
||||||
|
|
||||||
|
Malformed files are skipped with a warning.
|
||||||
|
"""
|
||||||
|
relations: List[RelationMeta] = []
|
||||||
|
for md_file in sorted(directory.glob("*.md")):
|
||||||
|
try:
|
||||||
|
relations.append(parse_relation_file(md_file))
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Skipping relation file %s: %s", md_file.name, exc)
|
||||||
|
return relations
|
||||||
Reference in New Issue
Block a user